CLR130 Clarity SQL Fundamentals
CLR130 Clarity SQL Fundamentals
Epic 2017
CLR130
SQL Fundamentals
Epic Systems Corporation
1979 Milky Way • Verona, WI 53593 • Voice: (608) 271-9000 • Fax: (608) 271-7237
www.epic.com
[email protected]
This guide is arranged for two-sided copying. Rev: March 6th, 2017
Table of Contents
Introduction 1•1
Functions 4•1
Handouts B•1
TOC•2 Table of Contents
Introduction
Welcome to Clarity SQL Fundamentals training!
This course will cover the basics of interpreting and creating SQL queries
including selecting data to display, filtering and grouping records, formatting
data, and proper use of SQL functions. In this class, you will strive to learn the
intricacies of SQL, as well as reinforce investigative techniques and gain exposure
to some of the more common Clarity tables.
This training companion is intended as a tool for learning the material presented
in class. Please use it as a place for notes on the topics and additional details
covered in class.
Recommendations for certification success:
Informative text boxes throughout the companion will draw your attention to
content of particular interest. Here are examples of types of text boxes you might
encounter:
Extra information related to a given topic. You do not need to know this
information for certification.
Our documents include Epic's standard copyright statement. Please do not remove
it. If you create new materials that contain Epic content, we ask that you include:
"This material contains confidential and copyrighted information of Epic Systems
Corporation" whenever that document contains Epic's screen shots or other
content based on our materials. You should also consider adding your own
copyright statement.
Be sure not to post Epic-related e-learnings or documentation on an unprotected
website that can be accessed by the general public. This includes both materials
that Epic has written and that you have written.
Contact Information
The table below provides you with contact information you may need during your
certification process:
Contact Used For
[email protected] The majority of your questions should be
directed to this email address. This address
connects you with our team of Cogito
trainers who can answer your questions on
anything related to reporting training. All
class-related questions should be directed to
this email address and not to the
Certification Environments or UserWeb
addresses listed below. If you aren't sure
about who to contact, use this email
address.
[email protected] Use this email address only if your access
specifically to the Certification
Environments is broken or lost. If you can
access the Certification Environments but
are having trouble logging in to
Hyperspace, contact
[email protected].
[email protected] Use this email address only if your access
specifically to the UserWeb is broken or
lost. If you have a question regarding
Cogito training contact
[email protected].
Direct email for your Epic representative For all exam reviews, contact your Epic
(either the implementers or your technical representative. You can find this
services representative) information by asking a member of your
own project team or by asking
[email protected].
[email protected] Use this email address to ask questions
about requesting, submitting, or taking
exams, as well as proctors for those exams.
[email protected] Use this email address to ask questions
about registering for classes at Epic.
[email protected] Use this email address to inquire about the
status of your paper certification or NVT
stickers.
After-Class Practice
After class, you will likely want to practice in the system and complete additional
in-class or if-you-have-time exercises. Please reference the following documents
available on Galaxy for information on how to log in and use our practice system:
Epic Resources
After leaving training at Epic, you will likely get asked questions that you do not
know the answers to. This table outlines supplemental resources that are available
to you that will help you find the information that you need. Here is a list of the
tools available and how they should be used:
Database Concepts
What is SQL?
SQL is an acronym for Structured Query Language. It is generally pronounced
“es-queue-el” or “see-kwell”. SQL can refer to a type of database or a language
used to communicate with that database. SQL has many implementations,
including MS-SQL and Oracle.
Database Objects
In your previous Clarity classes, much of the emphasis was placed on discussing
which ‘tables’ to use in reports. Most often, we were actually referring to
extracted tables. Extracted tables are what most business intelligence developers
envision when they think of Clarity tables. Extracted tables extract data from
Epic’s production database, usually from items in Chronicles, and store that data
in columns in Clarity. However, your Clarity database will contain other types of
database objects in addition to extracted tables.
Object Description
Extracted Tables Extracted tables are populated during ETL with data
directly from Chronicles. These make up the majority of a
Clarity database.
Examples: PATIENT, PAT_ENC_HSP, CLARITY_SER
Derived Tables Derived tables are tables populated from queries that run
against other Clarity tables. Using derived tables can
reduce the amount of work that business intelligence
developers must do to report on Clarity data; one derived
table might take the place of several extracted tables.
Derived tables can increase report efficiency; they can be
thought of as “pre-compiled and saved” reports. Derived
tables in Clarity typically start with D_ or F_.
Examples: F_SCHED_APPT, D_MU_MEASURES
Views Views are queries that are stored in the database. A view
can be accessed as if it were a table. Unlike tables,
however, views do not actually store results. This means
that using a view will not necessarily increase the
efficiency of a report. However, views can reduce the
amount of work that business intelligence developers must
do to report on Clarity data as well as reduce the
complexity of a query. Views in Clarity typically start
with a V_.
Examples: V_SCHED_APPT, V_ACCESS_LOG
Stored Procedures A SQL statement stored in your SQL Server so that it can
be run as needed. In Clarity, these are used to create
derived tables and are run after ETL completes.
Example: ESP_F_SCHED_APPT (the script that creates
F_SCHED_APPT)
String and Number are the most straightforward: all Strings become VARCHARs,
and all Numbers become NUMERICs or FLOATs.
Dates, Times, and Instants all become DATETIMEs. Recall in Chronicles that
dates, times, and instants are all stored in an “internal format” (number of days
since 12/31/1840 or number of seconds since midnight). These are converted into
actual dates or times during the ETL process. In order to fit into the DATETIME
format, most columns are also are modified to include both a date and a time.
Dates are extracted as midnight on the date. Times are extracted as the time on
1/1/1900. Instants are extracted as-is.
Contact Date Reals, as discussed in Clarity Fundamentals, remain a FLOAT, in
order to preserve the information they contain.
Database Administration
This class covers the information needed to create SQL queries to report off of a
Clarity database; it does not cover the information needed to administer that
Clarity database. In this way, the class differs from most other SQL courses in
that there are some very basic topics that this class does not cover. To report off
of Clarity, INSERT, CREATE, DELETE, and UPDATE commands are not needed.
These commands, which are typically involved in the Clarity ETL process, are
used to modify the database and are handled by your database administrator.
SELECT . . .
FROM . . .
CLARITY
The result of any SQL query is a table. This means that you can potentially
write queries and use them in place of other tables. This is discussed
further in Lesson 8 - Using Subqueries.
SQL queries must be written with their clauses in the following order:
SELECT
FROM
WHERE
GROUP BY
HAVING
ORDER BY
However, when implementing the query, SQL processes the clauses in the
following order:
FROM
WHERE
GROUP BY
HAVING
SELECT
ORDER BY
Because of this, when you are analyzing a SQL query, you may find it helpful to
read the query in this second order, not the order in which it is written.
ʺI [INI] [#]ʺ is shorthand for ʺitem number [#] in the master file [INI].ʺ
Therefore, I EPT .1 means ʺItem .1 in the EPT master file.ʺ
"R [INI] [ID]" is shorthand for "the record with the ID of [ID] in the
master file [INI]." Therefore, R EPT TR8R means "the record with the ID
of TR8R in the EPT master file."
Comments
Comments are sections of text within your query that are not treated as code; in
fact, they are completely ignored by SQL. Commenting code serves two
purposes: documentation and troubleshooting. Proper documentation with
comments enables both yourself and others to better use, understand, view, and
edit your queries. Comments can start at any point during the query, on their own
line or not. Line comments start with a double-dash (--), which will cause the rest
of the line to become a comment. Block comments start with a forward-slash
followed by an asterisk (/*), which will cause all text to be treated as a comment
until it reaches an asterisk followed by a forward-slash (*/).
Comments can also provide a method of troubleshooting, since they can be used
to temporarily disable sections of the code.
SELECTing Columns
Often, in Clarity, using SELECT * FROM [TABLE] will get you plenty of data, but
not much usable data. To provide flexibility, Clarity is structured in such a way
that many times you will not want to include all of the columns available to you.
Instead, you will want to choose specific columns for any given report.
The SELECT list is a comma-separated list of columns to display in your result
grid. To see only patient names and IDs, you could write the following query:
SELECT
PAT_NAME
,PAT_ID
FROM
PATIENT
Recall that the FROM clause is processed before the SELECT clause.
Building the query in the same order as it is processed can help you
ensure that you’re building it correctly. Also, building the FROM clause
first improves completion matching for the rest of the query.
These methods work for MS-SQL. For Oracle, you will have to use a
WHERE clause with the ROWNUM value.
The SELECT TOP statement can be used to limit results to a number of rows or a
percentage of total rows. This is typically used for spot checking a report or
previewing data from a large table or complex query. For example, for the
PATIENT table, SELECT TOP 5 * FROM PATIENT would return the first five
rows of every column in the table whereas SELECT TOP 1 PERCENT * FROM
PATIENT would return the first one percent of rows of every column in the table.
Aliasing
Aliasing allows you to assign new and temporary names to tables or columns.
These can be either from the database, in which case you are renaming them, or
they can be entirely new columns or 'tables' that don’t exist in the database.
There are many syntaxes for defining aliases, and several are version dependent.
You can use the AS keyword in an alias to make it explicit, but it is not required
(in Oracle, you can use the AS keyword to alias a column, but not a table). If the
alias has a space in it, it must be enclosed in double quotes or square brackets. In
MS-SQL, single quotes also work for columns (but not tables). We recommend
that you choose one aliasing syntax and stick with it for consistency. However,
you will see queries written by others that use different syntaxes.
SELECT
acc.ACCOUNT_ID AS ID
,acc.ACCOUNT_NAME "account name"
,acc.BIRTHDATE DOB
,acc.SSN [Social Security Number]
FROM
ACCOUNT acc
We recommend that you alias every table with a non-ambiguous alias, and that
you use those aliases whenever you reference the table or a column from it.
Crystal Reports and other reporting tools that auto-generate SQL code will
sometimes put the first reference to a table in the FROM clause in the
format of <DATABASE>.<SCHEMA>.<TABLE>. This is usually
followed by an alias, as in the example:
Clarity_2017_PRACTICE.dbo.CLARITY_SER CLARITY_SER
Sometimes, you will see quotes around each element, such as:
"Clarity_2017_PRACTICE"."dbo"."CLARITY_SER" "CLARITY_SER"
Style Choices
Consider the following SQL query:
SELECT
fact.PAT_ID "Patient ID"
,fact.PAT_NAME "Patient Name"
,fact.PAT_MRN_ID "Patient MRN"
,fact.AGE_YEARS "Age in Years"
,fact.CUR_PCP_NAME "Primary Care Provider"
FROM
V_PAT_FACT fact
There are several stylistic choices that have been made here, the first of which
is to make the code atomic. This means that each individual piece of code is
placed on its own line. In atomic code, it is easier to see what is happening
within different sections of code and to comment out single lines, temporarily
removing pieces of the query.
Placing commas at the start of the next line, as opposed to at the end of a line,
allows you to easily continue to edit a line without worrying about putting a
comma at the end. SQL ignores all whitespace, so the functionality is the same.
Epic recommends that Clarity be configured to be case-insensitive, so that
there is no difference between PATIENT and patient. However, in a query,
we recommend that you use uppercase for database objects such as tables,
columns, or functions, as well as for reserved words such as SELECT, OR, AS,
and AND. Lowercase should be used for aliases.
These style choices have no actual effect on the SQL query or the results. The
following code would return the same results:
Select Fact.PAT_ID "Patient ID", fact.PAT_NAME "Patient
Name",PAT_MRN_ID "Patient MRN",fact.AGE_YEARS "Age in
Years",CUR_PCP_NAME "Primary Care Provider" from v_pat_fact FACT
□ 2. Modify the FROM clause by adding the alias “ser” after the
CLARITY_SER. Your FROM statement should look like this:
FROM
CLARITY_SER ser
□ 3. Modify the SELECT clause by adding the table alias “ser” to each of the
columns. For example, the first column in your SELECT clause should
look like this:
ser.PROV_ID
□ 6. Add spacing between your column names and column aliases to make
your query easier to read.
Exercise 11: If Time Permits – Getting a List of Hospital
Encounters
Create and run a query to display the PAT_ID, PAT_ENC_CSN_ID,
ADMISSION_PROV_ID, DISCHARGE_PROV_ID, DEPARTMENT_ID,
HOSP_ADMSN_TIME, and HOSP_DISCH_TIME columns from the
PAT_ENC_HSP table. Use the alias “peh” for the PAT_ENC_HSP table.
SELECTing Constants
You can use the SELECT list to show a constant value, calculated values, or data
from columns in tables. We’ll investigate each of these different uses throughout
the class, but we’ll start with constant values first.
You can use SELECT followed by a comma delimited list of constant values,
such as numbers and strings. If you do not include a FROM clause, this query will
return a single row, with each value stored in its own column. For example,
SELECT
1
,2
,'Cogito Rocks'
produces the output
By itself, this isn’t a particularly useful query since it doesn’t actually pull any
data from the database. However, including constants in the SELECT list
becomes more useful when combined with other data from the database.
In Oracle SQL, to select a constant, you do need a FROM clause. There is a
table named DUAL which exists for this purpose, as in the example:
SELECT 1, 2, 'Cogito Rocks' FROM DUAL;
Selecting constants can be useful for adding labels to the data in your results grid.
Consider the following query:
SELECT
'Providers and Resources'
,'SER ID: ' + ser.PROV_ID "Provider ID"
,'Record Name: ' + ser.PROV_NAME "Provider Name"
FROM
CLARITY_SER ser
Putting single quotes (' ') around something indicates that it should be
treated as a varchar (string), even if its contents are otherwise numeric.
Review Questions
Review Key
1. Which clause in a SELECT statement is evaluated first?
True
False
/* Comment goes
here */
Aliasing
Exercise Answers
See the Cogito’s Introduction to Epic’s Certification Environments appendix to
access the answer key files.
Study Checklist
Make sure you can define the following key terms:
SQL query
SELECT clause
FROM clause
Alias
Make sure you can perform the following tasks:
Create a SQL query to list all of the rows and columns in a table
Add comments to a SQL query
Create a SQL query to list data from specific columns in a table
Create a SQL query to limit the number of rows returned
Create a SQL query to return only unique values for given column
Create a SQL query that uses aliases
Logical Expressions
Writing queries to select data from a table is often only the first part of writing a
report. Most of the time, there is specific information that you want to get out of
the tables. For instance, the PAT_ENC table contains one row for each patient
encounter. But what if you are only interested in office visits, or telephone
encounters? What if you are asked to write a report to show only patients with a
specific diagnosis in the last year, as opposed to a list of all patients? Consider a
report requesting only hospital encounters where the admitting provider and the
discharge provider were the same person; how would you go about doing that? In
this chapter, you’ll investigate how to express which rows in a table you seek.
PATIENT
PAT_ID PAT_NAME
1 Jewel
2 Neil
3 Nathan
4 Felicia
PATIENT
PAT_ID PAT_NAME
1 Jewel
PATIENT
PAT_ID PAT_NAME
2 Neil
3 Nathan
4 Felicia
Logical Expressions
The fundamental task in report writing is to ensure that all of the needed data and
no unnecessary data appears on a report. This is accomplished by making
statements about the results you would like to find. These statements are called
logical expressions.
Finding Nulls
NULL is never equal to anything, even itself. When NULL appears in a
comparison, there isn’t enough data to compute the expression. Consider the
following expression:
PATIENT.CITY = NULL
This expression is never TRUE and never FALSE, but rather is always
UNKNOWN. There is never enough data to perform the comparison; the right
side of the expression is always NULL. To find NULL values, the following
expression would be appropriate:
PATIENT.CITY IS NULL
Negating Expressions
When working with logical expressions, the word NOT can be added to reverse
TRUE and FALSE statements. However, UNKNOWN differs from FALSE in
that the statement NOT FALSE evaluates to TRUE, while the statement NOT
UNKNOWN evaluates to UNKNOWN.
Consider this expression from an earlier example:
NOT (PATIENT.CITY = 'Verona')
For the patients who live in Verona, this expression is now FALSE. For patients
who live in a city other than Verona, this statement is now TRUE. But for patients
with a NULL city, this expression is still UNKNOWN. If the patient has no
documented city, you don’t know if the patient lives in Verona. You also don’t
know if the patient does NOT live in Verona.
□ 3. PAT_STATUS_C IS NULL
The CASE statement will return the [value] associated with the first TRUE
[logical expression]. The case statement typically appears as a column in
the select list. Note the END, which is required with all CASE statements.
You are writing a report on encounters and want to identify
encounters that patients had with their primary care providers.
There is no column in PAT_ENC to show whether a visit was with the patient’s
PCP. However, there is a column for VISIT_PROV_ID, and another column for
PCP_PROV_ID. So, you can use a CASE statement to identify these encounters:
SELECT
PAT_ENC_CSN_ID
,CONTACT_DATE
,VISIT_PROV_ID
,PCP_PROV_ID
,CASE
WHEN VISIT_PROV_ID = PCP_PROV_ID
THEN 'Visit with PCP'
WHEN VISIT_PROV_ID <> PCP_PROV_ID
THEN 'Visit with other provider'
ELSE 'Missing Data'
END
FROM
PAT_ENC
The CASE statement can also specify a column or formula to compare against
possible values. This second format limits you to comparing values against only a
single static value or column. The implicit operator for the comparison is =.
CASE [column]
WHEN [value 1]
THEN [option 1]
WHEN [value 2]
THEN [option 2]
/* ... */
ELSE [default value]
END
/* ... */
For example, the following two queries accomplish the same task of converting
the IS_RESIDENT column in the CLARITY_SER table into a friendlier format.
SELECT
ser.PROV_ID
,ser.IS_RESIDENT
,CASE --Choose one of the following:
WHEN ser.IS_RESIDENT='Y' --The provider is a resident
THEN 'Resident'
WHEN ser.IS_RESIDENT = 'N' --The provider is not a resident
THEN 'Not a Resident'
ELSE 'No Value' --Default value
END "Interpretation" --End, and alias the column
FROM CLARITY_SER ser
In SQL, string constants such as 'Y' and 'Resident' are always placed
within single quotes. Unlike in Crystal Reports syntax, double and single
quotes perform different tasks in SQL. Double quotes are used for table
names, column names, and aliases such as "Provider ID".
Using the second format, the above statement can also be written like this:
SELECT
ser.PROV_ID
,ser.IS_RESIDENT
,CASE ser.IS_RESIDENT --Look at the value of this column
WHEN 'Y' --Is the value 'Y'
THEN 'Resident'
WHEN 'N' --Is the value 'N'
THEN 'Not a Resident'
ELSE 'No Value' --Default value
END "Interpretation" --End, and alias the column
FROM CLARITY_SER ser
In MS-SQL, date constants are written in single quotes. There are many
different formats accepted, like '1/2/2016' and 'Jan 2, 2016'
Medications
Write a query to display a list of medications. Medications are stored in the ERX
master file. Display the medication ID and the medication name. Then add a
result column for whether the medication is a mixture. A medication is a mixture
if the item ERX 405 is populated (not NULL).
Ambulatory Encounters
Write a query to display the reason for visit from patient encounters. Display the
encounter CSN and the contact date. Then add a result column for the reason for
visit, but if there is a custom reason for visit, display the custom reason entered
instead. The category item for reason for visit is EPT 18100.
Professional Billing
Write a query on professional billing transactions. Display the transaction ID, the
original amount as defined by the item ETR 17300, and the post date. Then add a
result column for an aging bucket for the transaction. If the post date was less than
30 days ago, this column should display ‘0-30’. If the post date was 31-60 days
ago, display ’31-60’. If the post date was more than 60 days ago, display ‘more
than 60.’ You may hard-code all relevant dates.
Exercise 5: Unknowns
□ 1. Run the following query. How many results do you get?
SELECT
*
FROM
PATIENT
WHERE
(CUR_PCP_PROV_ID = NULL)
□ 5. In SQL, can you use an equal sign (=) to find null values? Why did you
get the same number of results in the first two queries?
Clinical
Write a query that displays a list of the diagnoses in your system. Display the
diagnosis ID, diagnosis name, and the list of current ICD10 codes. Do not include
diagnoses without any ICD-10 codes. ICD-10 codes can be found in the item
EDG 4104.
Access
Write a query to find admission events (as defined by the item ADT 30). Display
the event ID, the event time, and the patient CSN.
Revenue
Find all guarantor accounts with a balance in the item EAR 20000. Display the
balance, the account ID, and the account name.
Cogito
Create a list of all the tables in Clarity that have been deprecated. Display the
table name, table ID, and table introduction. Clarity table metadata is stored in the
E0B master file.
SELECT
pat.PAT_ID
,pat.PAT_NAME
FROM
PATIENT pat
WHERE
pat.SEX_C = '2' --Patient is Male
AND
pat.BIRTH_DATE < '1984-01-01' --Patient was born before 1984
If both pieces of an AND expression are true, then the result of the AND is true. If
either piece is false, then the expression is false. True AND unknown, as well as
unknown AND unknown, will both evaluate to unknown.
SELECT
pe.PAT_ID
,pe.CONTACT_DATE
,pe.ENC_TYPE_C
FROM
PAT_ENC pe
WHERE
pe.ENC_TYPE_C = 101 --Generic 'Office Visit'
OR pe.ENC_TYPE_C = 1134 --Office Visit for new patient
OR pe.ENC_TYPE_C = 1135 --Office Visit for vip patient
If either piece of an OR expression is true, then the result of the OR is true. If both
pieces are false, then the expression is false. False OR unknown and unknown OR
unknown will evaluate to unknown.
Consider patients with different combinations of sexes and birth dates. Fill out the
table below with a “Y” if the patient will be included in the results of the above
query or an “N” if the patient will not be included.
Fill out the same table, remembering the difference between AND and OR logic.
In-Class Exercises
In this exercise you basically derived the following rules; you may find it
convenient to refer back to these formal statements of them in the future:
NOT (A OR B) = (NOT A) AND (NOT B)
NOT (A AND B) = (NOT A) OR (NOT B)
Indexing
Indexes can make your reports run faster. In order to use an index, you first have
to include an indexed column in the query. Indexed columns can improve query
performance when used in the WHERE clause. They also can improve performance
if used in the ON statement of a table join, which you will investigate later.
Note the use of the word “can” here, as opposed to “will.” SQL is a
declarative language, meaning that you use it to describe the results you
seek without actually specifying how to go about getting those results.
Using indexes in your queries gives the database an opportunity to
leverage them to improve performance, but there is no guarantee that it
will actually do so.
You can find a list of documented indexes in the Clarity Compass in the Table
Summary screen.
Index information for the PATIENT table using sp_help. You will need to scroll to
find this informaiton in your results grid.
In Oracle, query the table DBA_IND_COLUMNS to find all existing
indexes for the tables in your database.
So what exactly is an index? If you wanted to find every patient who has Dr. Whitecoat
as their current general primary care provider, your first thought might be to search
through every row of the PATIENT table and check the CUR_PCP_PROV_ID to see if
Dr. Whitecoat’s ID appeared. This would be possible, but it might take a while. If you
work at a large facility it might take a very long while.
… …
… …
Review Questions
Review Key
((True and False) or (True or False)) and ((True and True) or False)
(False or True) and (True or False)
True and True
True
Exercise Answers
See the Cogito’s Introduction to Epic’s Certification Environments appendix to
access the answer key files.
□ 3. PAT_STATUS_C IS NULL
□ 3. PAT_STATUS_C in (1,2,NULL)
Exercise 5: Unknowns
□ 1. Run the following query. How many results do you get? 0
□ 2. Run the following query. How many results do you get? 0
□ 3. Run the following query. How many results do you get? (Several. Since
the training database updates periodically, the record count may vary.)
□ 4. Run the following query. How many results do you get? (Several. Since
the training database updates periodically, the record count may vary.)
□ 5. In SQL, should you use an equal sign (=) to find null values? Why did you
get the same number of results in the first two queries? No, you should use
the IS operator instead. If you use =, the result will always be unknown,
and the opposite of unknown is unknown. Unknown results are excluded
from the WHERE clause.
Study Checklist
Make sure you can define the following key terms:
Logical Expression
CASE statement
WHERE clause
Make sure you can perform the following tasks:
Write an expression that compares a column to a value or another
column
Combine multiple comparisons using logical operators
View the indexes for a Clarity table
Make sure you fully understand and can explain the following concepts:
The difference between SELECT TOP and the WHERE clause
Logical operators and complex logic
Index
Reference 25
Replace Epic Standard SQL Functions with Database-Specific Code 25
After-Class Exercises 27
Exercise 1: Patients with Upcoming Birthdays 27
Reviewing the Chapter 29
Review Questions 29
Review Key 30
Exercise Answers 31
Exercise 1: Functions and Arguments 31
Exercise 2: Discovering Functions 31
Conversion Functions 31
Date and Time Functions 32
String Functions 34
Null Functions 35
Exercise 3: Using Functions with Dates 36
Study Checklist 36
Functions
Generating reports by simply pulling data from the database is useful, but limited.
In this lesson, you will investigate different functions used in SQL queries. These
will allow you to add more functionality and flexibility to your reports, by
combining and manipulating data from the database in interesting ways.
DATEADD will take the number of the intervals (as defined by the
datepart) and add it to (or subtract it from) the date. Fill in the blanks in
the queries below:
When you use a function in the SELECT list, you are creating a new column for
your results. For example, in the query:
SELECT
CURRENT_TIMESTAMP "Current Date"
you create a column called Current Date, which contains the value of the date
and time on the server when the query is run.
A small number of functions which take no arguments, such as
CURRENT_TIMESTAMP, do not require parentheses.
In this query, the MONTH function will run once per row in the PATIENT table;
for each patient, the query will return the patient’s ID and the month in which the
patient was born.
Functions require arguments of the correct data type in the correct order. For
example, the DATEADD function requires one datepart, one number, and one
date or datetime, in that order. Any invalid input will cause an error, and the query
will not run.
Using functions will allow you to filter your results on more than just the raw
data—which may or may not be necessary. For instance, consider a report request
to show only patient encounters that took place in May 2015. There are several
ways to solve this problem, but each can alter the efficiency of the query.
CONTACT_DATE is an indexed column, so it can make your queries more
efficient if used in the WHERE clause. To take advantage of the index, you
should compare CONTACT_DATE to other dates:
SELECT
*
FROM
PAT_ENC pe
WHERE
pe.CONTACT_DATE >= '5/1/2015'
and pe.CONTACT_DATE < '6/1/2015'
It may be tempting to use a function for this filter. Consider the following query:
SELECT
*
FROM
PAT_ENC pe
WHERE
YEAR(pe.CONTACT_DATE) = 2015
and MONTH(pe.CONTACT_DATE) = 5
This finds the year-part of the contact date and matches it to 2015, then finds the
month-part of the contact date and matches it to 5.
The second query will be considerably less efficient than the first. The first query
is searching based on the value of CONTACT_DATE, which is indexed.
Therefore, it does not need to evaluate every row of PAT_ENC. The second query
is searching based on the output of the functions YEAR(CONTACT_DATE) and
MONTH(CONTACT_DATE), which are not indexed. Therefore, it will need to evaluate
the output of the functions for every row of PAT_ENC.
As a general rule, avoid using indexed columns inside functions in the WHERE
clause. The query will not use an index if the column is an argument to a function.
SQL Functions
Conversion Functions
Conversion functions change data from one type to another. In Clarity, some
numeric data is stored as strings, and to use the data in a calculation, it must be
converted into numbers. Also, to format numeric data, it needs to be converted
into strings. The process of changing data types is often referred to as “casting”.
You are creating a SQL query that will be used in a Crystal Report.
To eventually group the report, you want one column that displays
the name of a department with its department ID in square brackets.
________________________________________________
SELECT
DEPARTMENT_NAME + ' [' + CAST(DEPARTMENT_ID as varchar) + ']'
FROM
CLARITY_DEP
________________________________________________
CAST() is a function that works on both MS-SQL and Oracle. Another
function, CONVERT(), serves a similar purpose but works only on MS-
SQL. The syntax for CONVERT() is:
CONVERT(varchar,DEPARTMENT_ID)
Your organization needs a list of all patients and their dates of birth.
The BIRTH_DATE field is a datetime in Clarity, but it often doesn’t
extract a time from Chronicles and defaults to midnight. You want to
change the format of this date to mm/dd/yyyy, with no time listed.
□ 5. Run the following query:
SELECT
PAT_NAME
,BIRTH_DATE
FROM
PATIENT
SELECT
PAT_NAME
,CONVERT(varchar, BIRTH_DATE)
FROM
PATIENT
Now, the BIRTH_DATE column looks different, but it still displays a
time of 12:00AM. You need to find a format that will hide the time part.
The F1 help key does not work in the Certification Environments. If you
are working there, search for 'CONVERT()' on
https://msdn.microsoft.com/ and select the first result.
________________________________________________
Change your query to display the BIRTH_DATE in this format:
SELECT
PAT_NAME
,CONVERT(varchar, BIRTH_DATE, 101)
FROM
PATIENT
SELECT
CURRENT_TIMESTAMP
________________________________________________
□ 12. Create a new query that displays the CSN, Contact Date, and Department
ID for all rows of the PAT_ENC table. Your query will look similar to the
following:
SELECT
PAT_ENC_CSN_ID
,CONTACT_DATE
,DEPARTMENT_ID
FROM
PAT_ENC
This query returns no results. An equals sign is not appropriate here; this
query looks for contacts at the exact millisecond the query is run.
When filtering based on dates, you will often need to use a range.
CURRENT_TIMESTAMP might work as a lower bound for the range, but the
upper bound needs to be the date 30 days from now.
□ 14. Remove the WHERE clause, then modify your query to the following:
SELECT
PAT_ENC_CSN_ID
,CONTACT_DATE
,DEPARTMENT_ID
,DATEADD(d,30,CURRENT_TIMESTAMP)
FROM
PAT_ENC
________________________________________________
Remove this from your SELECT list. For your report, you need contacts
within the next thirty days.
□ 16. Add the following WHERE clause to the end of your query:
WHERE
CONTACT_DATE between CURRENT_TIMESTAMP and
DATEADD(d,30,CURRENT_TIMESTAMP)
You need to add a column to your results grid to show how many
days away these contacts are.
□ 17. Type DATEDIFF() into your SELECT list. Place your cursor in the function
name, then open online help by pressing F1.
The F1 help key does not work in the Certification Environments. If you
are working there, search for 'DATEDIFF()' on
https://msdn.microsoft.com/ and select the first result.
________________________________________________
□ 19. Add a column to your query to display how many days away each
upcoming encounter is.
SELECT
PAT_ENC_CSN_ID
,CONTACT_DATE
,DEPARTMENT_ID
,DATEDIFF(d,CURRENT_TIMESTAMP,CONTACT_DATE)
FROM
PAT_ENC
WHERE
CONTACT_DATE between CURRENT_TIMESTAMP and DATEADD(d,30,
CURRENT_TIMESTAMP)
You now need to find all of the encounters that occurred last year up
to today's date last year.
□ 20. Clear out your existing WHERE clause. Type DATEFROMPARTS() into your
WHERE clause and press F1 (or search for it on the Microsoft website). This
function will be useful for finding these encounters.
What is the datatype of all of the arguments for this function?
__________________________________
__________________________________
□ 21. Change your WHERE clause to find contacts that occurred last year up to
today's date last year. You can also use the functions YEAR(), MONTH(),
DAY(), and CURRENT_TIMESTAMP.
String Functions
You've created a formula that finds the average charge amount for
the inpatient pharmacy. However, it's not creating a number that
looks like a dollar amount.
□ 22. Type the following new query into SQL Management Studio:
SELECT AVG(CHARGE)
FROM V_RX_CHARGES
□ 23. Type STR()into your SELECT list and press F1, or search for the function
on Microsoft’s website. Look through the resulting help page to find
information on the STR() function.
In the Microsoft SQL function help text, optional variables appear in
square brackets.
□ 24. You want to turn your dollar amount into a string, 5 characters long, with
2 decimals. Use the STR() function on your average charge amount to
create this string.
Your select list should now include the following:
STR(AVG(CHARGE),5,2)
□ 25. Since the dollar amount is now a string, you may also concatenate a dollar
sign to the front of it. Your query should now look like the following:
SELECT
AVG(CHARGE)
,'$' + STR(AVG(CHARGE),5,2)
FROM V_RX_CHARGES
You need to display each provider's name and title in a single field.
□ 27. You want to put PROV_NAME and CLINICIAN_TITLE into the same field.
Try modifying your SELECT list to the following:
SELECT
PROV_NAME + ', ' + CLINICIAN_TITLE
This doesn’t work, because the concatenation operator (+) can’t handle
one of its operands being NULL. If just one of the values being
concatenated is NULL, the concatenation returns NULL.
This problem is specific to MS-SQL. In Oracle, the double-pipe operator
( || ) can concatenate fields even when one is NULL.
□ 28. Type CONCAT()into your SELECT list and press F1. Look through the
resulting help page to find information on the CONCAT() function.
Instead of using the + operator to concatenate PROV_NAME and
CLINICIAN_TITLE, use the CONCAT() function.
The CONCAT() function is only available in MS-SQL. In Oracle, use the
double-pipe operator ( || ).
Null Functions
Different patients should receive different mailings, depending on
which location they visit. Some patients will have a primary location
specified; so for these patients you want to find the ID of the primary
location. If the patient doesn’t have a primary location specified, you
want to use their city to find the closest location. If the patient
doesn’t have a city either, you should display a default string.
□ 29. Type the following new query into SQL Management Studio and run it:
SELECT
COALESCE(CITY,'*Unknown Location')
FROM
PATIENT
__________________________________________________________
Location IDs are numeric, while cities are strings. COALESCE() can take
as many arguments as needed, but they all must be the same data type.
□ 31. Fix the problem using another function from an earlier part of this
exercise. Feel free to modify your query further if you’d like.
Your final query may look similar to the one below:
SELECT
COALESCE(CAST(CUR_PRIM_LOC_ID as varchar),CITY,'*Unknown
Location')
FROM
PATIENT
CURRENT_TIMESTAMP Returns the current date and time. Does not need
parentheses.
Strings
STR(float_expression, Returns the number in float_expression as a string,
[length,decimals]) with a defined total length and number of decimals.
Null
COALESCE(input1, Returns the first non-null input.
input2[,input3, ...])
Here are a few more functions that may be useful. Keep in mind that there are
many more functions in SQL than can be covered here. If you have time, try
writing queries using these functions.
LEN(string_expression)
LEN(string_expression) Returns the number of characters in any
string_expression.
CHARINDEX(expressionToFind, Returns the number of the character in
expressionToSearch, expressionToSearch where the first
[start_location])
occurrence of expressionToFind begins.
Example: CHARINDEX('c','abcde') = 3.
SUBSTRING(expression, Returns part of a string, starting at the start
start, length)
position, and continuing for a given length.
ISNULL(check_expression, If check_expression is NULL, returns
replacement_value) replacement_value. Otherwise, returns
check_expression.
Similar to COALESCE(), however it can only
handle two expressions. Is processed more
quickly than COALESCE().
In Oracle, this function is called NVL( ).
HAT Y M D
2013-03-14
2013-06-23
SELECT
peh.HOSP_ADMSN_TIME as HAT
,peh.HOSP_DISCH_TIME as HDT
,DATEADD(D,30, peh.HOSP_DISCH_TIME) as DA
,DATEDIFF(D, peh.HOSP_ADMSN_TIME, peh.HOSP_DISCH_TIME) as DD
FROM
PAT_ENC_HSP peh
HAT HDT DA DD
2013-03-14 2013-03-31
2013-06-23 NULL
SELECT
peh.HOSP_ADMSN_TIME as HAT
,peh.HOSP_DISCH_TIME as HDT
,COALESCE(peh.HOSP_DISCH_TIME, peh.HOSP_ADMSN_TIME,
CURRENT_TIMESTAMP) as C
FROM
PAT_ENC_HSP peh
HAT HDT C
2013-03-14 2013-03-31
2013-06-23 NULL
NULL NULL
Different Epic organizations will use different Clarity database types (Microsoft
SQL Server, Oracle, Teradata, etc). This requires different code to be developed
depending on the underlying database. To help avoid this in Epic-released reports,
Epic created several User Defined Functions (UDFs) that make our SQL code
database-independent. The Epic Standard SQL Functions are a library of UDFs
developed and released by Epic.
This section aims to make you aware of these Epic Standard SQL Functions and
able to interpret their use in Epic-released reports. Generally, you should not use
Epic Standard SQL Functions in your custom-created reports built for use within
your organization. You are aware of your organization’s database type and can
create code using that specific syntax, which will be more efficient than using the
Epic functions that check the platform each time they execute.
Epic Standard SQL Functions are easy to identify in Epic released queries; they
are all stored in the EPIC_UTIL schema. The following is a short description of
Epic Standard SQL functions you might see in Epic-released reports.
These functions are used to convert dates and times to different formats.
Function Purpose Syntax
Get the current system date EPIC_UTIL.EFN_CURR_
EFN_CURR_DTTM and time. DTTM()
String Functions
These functions are used to calculate information about and modify strings.
EPIC_UTIL.EFN_CONCA
EFN_CONCAT Concatenate two strings.
T(string1, string2)
EPIC_UTIL.EFN_NAME_
Return the name with or
WEXTID(dataID,
without your defined
EFN_NAME_ lookupID, lookupName,
external ID of the lookup
WEXTID lookupExternalLabel,
record when using a left
lookupGenericDescription,
outer join with the dataID.
showLabel)
IntraConnect Functions
Reference
Replace Epic Standard SQL Functions with Database-
Specific Code
Let’s take a look at the syntax you could use to make several of the Epic Standard
SQL Functions specific to your database.
First, let’s take a look at EFN_CONCAT. This Epic Standard SQL Function is
used to concatenate two columns.
You want to display the first and middle names of patients for display on a
report, concatenated together without the patients’ last names. There are
separate columns in the PATIENT table for FIRST_NAME,
MIDDLE_NAME, and LAST_NAME.
You’ll develop the database-specific function to do so in Microsoft SQL
Management studio. First, create a query to display the data as-is and
when formatted using the EFN_CONCAT function:
SELECT
pat.PAT_FIRST_NAME
,pat.PAT_MIDDLE_NAME
,EPIC_UTIL.EFN_CONCAT(pat.PAT_FIRST_NAME
,pat.PAT_MIDDLE_NAME)
FROM
PATIENT pat
Another limitation of the Epic Standard SQL function is that without
nesting multiple function calls, it only concatenates two strings, meaning
that there will be no space between the patient’s first name and middle
initial. Replacing the EFN_CONCAT function with database-specific
concatenation will be more flexible.
Concatenation for Oracle is achieved using a double-pipe “||”, and in SQL
server using a plus “+”. In training we have SQL Server, so we would
replace the function with:
SELECT
pat.PAT_FIRST_NAME
,pat.PAT_MIDDLE_NAME
,pat.PAT_FIRST_NAME + ' ' + pat.PAT_MIDDLE_NAME
FROM
PATIENT pat
Next, let’s take a look at EFN_TRUNC_DT. This function is used to remove the
time from a field that stores both date and time, so it returns the correct date with
time showing midnight.
MyChart, Epic’s web-based chart system, has messaging functionality that
allows providers to send patients information, such as lab results or visit
follow-up, instantly and securely. When reporting on these MyChart
messages, one piece of data that can be displayed is the date and time a
message is created.
This value is stored as a date-time field, in
MYC_MESG.CREATED_TIME. Instead of displaying both date and time,
you would like just the date portion of this field.
You’ll develop the database-specific function to do so in Microsoft SQL
Management studio. First, create a query to display the data as-is and when
formatted using the EFN_TRUNC_DT function:
SELECT
myc.CREATED_TIME
,EPIC_UTIL.EFN_TRUNC_DT(myc.CREATED_TIME)
FROM
MYC_MESG myc
Refer to the list of SQL functions earlier in this chapter. Which would you
use to replace the EFN_TRUNC_DT function for your organization’s
database?
In training, we have SQL Server, so we’ll use the CONVERT function. If
you have Oracle at your organization you would use the TRUNC function.
Since what you want is a datetime format, you’ll have to use the
CONVERT function twice, once to remove the time field completely, then
again to add it back in so it will default to midnight:
SELECT
myc.CREATED_TIME
,CONVERT(datetime,CONVERT(date,myc.CREATED_TIME))
FROM
MYC_MESG myc
After-Class Exercises
Review Questions
Review Key
1. True or false: All functions require the use of parentheses, regardless of
whether they take any arguments.
True
True
Exercise Answers
See the Cogito’s Introduction to Epic’s Certification Environments appendix to
access the answer key files.
Conversion Functions
□ 1. Try to run the following query:
SELECT
DEPARTMENT_NAME + ' [' + DEPARTMENT_ID + ']'
FROM
CLARITY_DEP
□ 2. What error is returned?
Error converting data type varchar to numeric.
SELECT
CURRENT_TIMESTAMP
□ 11. What does the CURRENT_TIMESTAMP function return?
The current date and time
□ 12. Create a query that displays the CSN, Contact Date, and Department ID
for all rows of the PAT_ENC table. Your query will look similar to the
following:
SELECT
PAT_ENC_CSN_ID
,CONTACT_DATE
,DEPARTMENT_ID
FROM
PAT_ENC
□ 14. Remove the WHERE clause, then modify your query to the following:
SELECT
PAT_ENC_CSN_ID
,CONTACT_DATE
,DEPARTMENT_ID
,DATEADD(d,30,CURRENT_TIMESTAMP)
FROM
PAT_ENC
WHERE
CONTACT_DATE between CURRENT_TIMESTAMP and
DATEADD(d,30,CURRENT_TIMESTAMP)
□ 17. Type DATEDIFF() into your SELECT list. Place your cursor in the function
name, then open online help by pressing F1.
□ 20. Clear out your existing WHERE clause. Type DATEFROMPARTS() into your
WHERE clause and press F1 (or search for it on the Microsoft website). This
function will be useful for finding these encounters.
What is the datatype of all of the arguments for this function?
Integer
What is the return type for this function?
Date
□ 21. Change your WHERE clause to find contacts that occurred last year up to
today's date last year. You will also need the functions YEAR(), MONTH(),
DAY(), and CURRENT_TIMESTAMP.
SELECT
PAT_ENC_CSN_ID
,CONTACT_DATE
,DEPARTMENT_ID
,DATEDIFF(d,CURRENT_TIMESTAMP,CONTACT_DATE)
FROM
PAT_ENC
WHERE
CONTACT_DATE <= DATEFROMPARTS(YEAR(CURRENT_TIMESTAMP)-1,
MONTH(CURRENT_TIMESTAMP),DAY(CURRENT_TIMESTAMP))
and YEAR(CONTACT_DATE) = YEAR(CURRENT_TIMESTAMP)-1
String Functions
□ 22. Type the following new query into SQL Management Studio:
SELECT AVG(CHARGE)
FROM V_RX_CHARGES
□ 23. Type STR()into your SELECT list and press F1, or search for the function
on Microsoft’s website. Look through the resulting help page to find
information on the STR() function.
□ 24. You want to turn your dollar amount into a string, 5 characters long, with
2 decimals. Use the STR() function on your average charge amount to
create this string.
SELECT
AVG(CHARGE)
,STR(AVG(CHARGE),5,2)
FROM V_RX_CHARGES
□ 25. Since the dollar amount is now a string, you may also concatenate a dollar
sign to the front of it. Your query should now look like the following:
SELECT
AVG(CHARGE)
,'$' + STR(AVG(CHARGE),5,2)
FROM V_RX_CHARGES
□ 27. You want to put PROV_NAME and CLINICIAN_TITLE into the same field.
Try modifying your SELECT list to the following:
SELECT
PROV_NAME + ', ' + CLINICIAN_TITLE
□ 28. Type CONCAT() into your SELECT list and press F1. Look through the
resulting help page to find information on the CONCAT() function.
Instead of using the + operator to concatenate PROV_NAME and
CLINICIAN_TITLE, use the CONCAT() function. Modify your query to the
following:
SELECT
CONCAT(PROV_NAME,', ',CLINICIAN_TITLE)
FROM
CLARITY_SER
Null Functions
□ 29. Type the following new query into SQL Management Studio and run it:
SELECT
COALESCE(CITY,'*Unknown Location')
FROM
PATIENT
HAT HDT DA DD
2013-03-14 2013-03-31 2013-04-30 17
2013-06-23 NULL NULL NULL
HAT HDT C
2013-03-14 2013-03-31 2013-03-31
2013-06-23 NULL 2013-06-23
NULL NULL *YYYY-MM-DD
Where *YYYY-MM-DD is the date that the query is run.
Study Checklist
Make sure you can define the following key terms:
SQL function
Argument
Conversion functions
Date and Time functions
String functions
Null functions
Epic Standard SQL Functions
Make sure you can perform the following tasks:
Replace Epic Standard SQL Functions with database-specific
functions in queries and reports
Find and use documentation for common SQL functions
Make sure you fully understand and can explain the following concepts:
Benefits of using functions
Using functions in the WHERE clause efficiently
Advantages and disadvantages of Epic Standard SQL functions
Review Key 18
Exercise Answers 19
Exercise 2: Aggregate Functions 19
Exercise 3: Limitations of the Aggregate Functions 19
Exercise 4: GROUP BY 20
Study Checklist 22
Big Picture
PAT_ID PROV_ID
Z15669 TRN041
Z1599 E1063
Z1603 E1022
Z1752 1066
Z2629 E1063
Z2633 E1022
Z2677 1066
Z3249 E1022
Patients from database
PAT_ID PROV_ID
Z1752 1066
Z2677 1066
Z1603 E1022
Z2633 E1022
Z3249 E1022
Z1599 E1063
Z2629 E1063
Z15669 TRN041
Sorted by provider
Sorting
Frequently, report requestors want their data organized and grouped together. One
such request would be to have a list of encounters, ‘grouped by department’. In
Crystal Reports, this is done using the Group Expert, and results in another level
of report in which you can place information. In SQL, however, we control that
level of grouping using the ORDER BY clause, and sort the data together into a
meaningful sequence. For example, you may want to sort in alphabetical order or
from highest to lowest outstanding balance. In SQL, we can control the order in
which information is resulted by using the ORDER BY clause. A column does not
need to appear in the SELECT clause to be able to sort by it.
In Crystal Reports, when you group a report by a field, the equivalent SQL
command Crystal actually uses is ORDER BY; Crystal then uses the fact
that the data is ordered to insert the new levels of the report.
ORDER BY is followed by the columns that you want to sort in ascending value, in
order of precedence. To do a reverse sort, add the DESC keyword after the
column. This is particularly effective at sorting dates, as in the following example,
where the DESC keyword will put the most recent dates at the top of the set.
SELECT
*
FROM
PAT_ENC pe
ORDER BY
pe.CONTACT_DATE DESC
You are also able to use column aliases from the SELECT statement list in the
ORDER BY clause. This is useful for sorting by the output of functions.
Aggregating Results
Aggregate functions perform a specific operation over all rows in a group.
Aggregate functions differ from regular functions in that they take many rows'
worth of input and return one row of output.
You use the above table in the FROM clause of a new query. Using the above
data, fill in the “Result” column below
COUNT(Age)
COUNT(value) Counts all non-null COUNT(SEX_C)
values
COUNT(PAT_NAME)
Averages all non-null
AVG(value) AVG(Age)
values
Returns the highest
MAX(value) MAX(CONTACT_DATE)
value
Returns the lowest
MIN(value) MIN(SEX_C)
value
Returns the sum (total)
SUM(value) SUM(Age)
of all non-null values
□ 2. Assuming that there are 50 rows in the PATIENT table, how many rows
would you expect this query to return?
□ 4. Assuming that there are 50 rows in the PATIENT table, how many rows
would you expect this query to return?
The last query in this exercise is attempting to perform two mutually exclusive
actions. First, it is attempting to generate a list of the patient IDs that are stored in
the patient table. Then it takes the single value of a count of all the rows in the
table. SQL makes no assumptions about how you want to combine these values,
and aborts the attempt to do so.
Grouping
In your previous Crystal Reports class, you created a report which grouped
patients by their Primary Care Provider (PCP). Grouping in Crystal Reports is
accomplished through the use of the Group Expert. Once the data is grouped, you
are able to display summary information, such as the count of patients per PCP,
while also displaying detailed information about each of the patients.
You are still able to aggregate the data, but aren’t able to view individual patient
information. You are only able to display the columns that are included in the
GROUP BY clause.
Once all of the data is filed into a group, your SELECT list now refers to the
groups, not the individual rows. So, there are rules about what you can SELECT
when you have a GROUP BY clause:
- You can SELECT the field(s) in your GROUP BY clause, i.e. the ‘labels’
from the groups, and
- You can SELECT any aggregate functions
o The aggregate functions will only apply to rows within each group.
Exercise 4: GROUP BY
Use the following query to answer the questions below.
SELECT
op.AUTHRZING_PROV_ID
,op.PROC_ID
,COUNT(op.ORDER_PROC_ID) '# Orders'
,COUNT(DISTINCT op.PAT_ID) '# Pats'
FROM
ORDER_PROC op
GROUP BY
op.AUTHRZING_PROV_ID
,op.PROC_ID
ORDER BY
op.AUTHRZING_PROV_ID
,op.PROC_ID
When you use GROUP BY, you can aggregate on any columns in the tables
involved in the query. However, you can only display columns that are not in an
aggregate function if you include that column in the GROUP BY clause.
You want to show a count of patients by provider. If you ran the following query:
SELECT
CUR_PCP_PROV_ID
,COUNT(*)
FROM
V_PAT_FACT
GROUP BY
CUR_PCP_PROV_ID
you would see a count of patients for each CUR_PCP_PROV_ID. But what if you
wanted to display the provider name?
If you ran the following query:
SELECT
CUR_PCP_PROV_ID
,CUR_PCP_NAME
,COUNT(*)
FROM
V_PAT_FACT
GROUP BY
CUR_PCP_PROV_ID
you’d get an error. You can’t have anything in your SELECT list that isn’t in your
GROUP BY clause or an aggregate function.
Consider the contents of the groups that are created:
There’s only one unique CUR_PCP_NAME in each group, but SQL doesn’t
know that. If you want to display CUR_PCP_NAME, you have to either add it to
your GROUP BY clause or put it inside an aggregate function. As it turns out,
both are viable options. Often using the aggregate function will be more efficient,
but this can depend on several factors.
You will come across seemingly superfluous aggregate functions in SELECT
lists. Consider the following query:
SELECT
CUR_PCP_PROV_ID
,MAX(CUR_PCP_NAME)
,COUNT(*)
FROM
V_PAT_FACT
GROUP BY
CUR_PCP_PROV_ID
This will run without error. It will return the provider ID, provider name, and a
count of that provider’s patients. Since all of the CUR_PCP_NAME values are
the same in each group, the maximum (or minimum) will be the provider name.
Another solution would be the following:
SELECT
CUR_PCP_PROV_ID
,CUR_PCP_NAME
,COUNT(*)
FROM
V_PAT_FACT
GROUP BY
CUR_PCP_PROV_ID,
CUR_PCP_NAME
Since there is only one CUR_PCP_NAME per CUR_PCP_PROV_ID, adding this
additional column to the GROUP BY clause will not increase the number of
groups. You must still group by CUR_PCP_PROV_ID, since CUR_PCP_NAME
is not necessarily unique. This solution may be less efficient in some cases.
Review Questions
2. What keyword can you use to change the direction of a sort in SQL?
Review Key
1. True or False: grouping in SQL is equivalent to grouping in Crystal
2. What keyword can you use to change the direction of a sort in SQL?
DESC
Exercise Answers
See the Cogito’s Introduction to Epic’s Certification Environments appendix to
access the answer key files.
□ 2. Assuming that there are 50 rows in the PATIENT table, how many rows
would you expect this query to return?
50
□ 4. Assuming that there are 50 rows in the PATIENT table, how many rows
would you expect this query to return?
1
Exercise 4: GROUP BY
□ 1. What table is the query using?
ORDER_PROC
The number of unique patients for whom each provider has ordered that
procedure.
Study Checklist
Make sure you can define the following key terms:
ORDER BY clause
Aggregate functions
GROUP BY clause
HAVING clause
Make sure you can perform the following tasks:
Write a SQL query with sorting
Write a SQL query with aggregate functions
Write a SQL query with grouping
Make sure you fully understand and can explain the following concepts:
Grouping in a SQL query vs. grouping in Crystal Reports
As you write a query, make sure you have a reason and a purpose for each
database object you include in the query. Generally, if you can write a
query with fewer tables, the query will run faster.
Creating a Join
When you are creating a join in a SQL query, you need to know several things
about the join before you can create it. You need to know:
The starting table of the join
The ending table of the join
The columns that act as foreign keys for the join
The join type that should be used
You are creating a report to display patients and the names of their current
general primary care providers.
For this report, you will need two tables: the PATIENT table for the patients’
names, and the CLARITY_SER table for the providers’ names. To determine
which columns you should link together, you could use the Clarity Compass to
investigate the columns in each table. When viewing the Foreign Key Information
for the PATIENT table, you see the column you’ll use to link to the
CLARITY_SER table:
Once you know which columns to join, you need to decide on the join type to use.
Join types are described in detail in the Clarity Data Model Fundamentals course.
In a SQL query, you use the JOIN statement to join database objects. The JOIN
statement is part of the FROM clause. For example:
SELECT
[columns]
FROM [table 1]
[JOIN TYPE] [table 2]
ON [expression]
For an inner join, the server goes through every combination of rows from the two
tables and evaluates the expression for each pair. If the expression is true, then the
combined row is included in the result set. The expression is typically of the form:
[table 1.foreign key] = [table 2.column]
Using this pattern with the query described above to join PATIENT and
CLARITY_SER, you would create the following query.
SELECT
pat.PAT_NAME
,ser.PROV_NAME
FROM PATIENT pat
LEFT OUTER JOIN CLARITY_SER ser
ON pat.CUR_PCP_PROV_ID = ser.PROV_ID
When writing queries with multiple database objects, it’s very important to
use table aliases whenever referring to columns. Otherwise, you will likely
encounter an “ambiguous column name” error.
For example, if you add COUNTY_C to the SELECT list without a table alias,
SQL won’t know if you want to select PATIENT.COUNTY_C (patients’
counties) or CLARITY_SER.COUNTY_C (providers’ counties).
You also want to display the counties in which the patients live. Counties
are stored as category numbers in the PATIENT table, and you need a
ZC_ table to find the name of the counties.
First, identify the table and columns used in this join. This can be achieved using
the Clarity Compass in Hyperspace.
To add another join to a SQL query, simply repeat the same syntax in your FROM
clause, starting with the next join type:
SELECT
pat.PAT_NAME
,ptcounty.NAME "Patient County"
,ser.PROV_NAME
FROM PATIENT pat
LEFT OUTER JOIN CLARITY_SER ser
ON pat.CUR_PCP_PROV_ID = ser.PROV_ID
LEFT OUTER JOIN ZC_COUNTY ptcounty
ON pat.COUNTY_C = ptcounty.COUNTY_C
You also want to display the county in which the provider works.
To display two different counties on the same row of your result grid, you’ll need
two copies of ZC_COUNTY. To do this, join to the table again, but use a
different alias with the second join:
SELECT
pat.PAT_NAME
,ptcounty.NAME "Patient County"
,ser.PROV_NAME
,provcounty.NAME "Provider County"
FROM PATIENT pat
LEFT OUTER JOIN CLARITY_SER ser
ON pat.CUR_PCP_PROV_ID = ser.PROV_ID
LEFT OUTER JOIN ZC_COUNTY ptcounty
ON pat.COUNTY_C = ptcounty.COUNTY_C
LEFT OUTER JOIN ZC_COUNTY provcounty
ON ser.COUNTY_C = provcounty.COUNTY_C
If you would like additional resources in creating your query, follow the steps
below.
□ 3. Begin building your query by creating the start of your FROM clause.
Include the main database object you identified above as the first table
you list in your FROM clause. This should be the PAT_ENC table. Give
this table an alias.
□ 4. For provider, patient, and department you’ll need more than just the IDs –
you want to display the names of those attributes.
a. What additional table will you need to display the patient name?
_____________________________
b. What additional table will you need to display the visit provider’s
name?
_____________________________
c. What additional table will you need to display the visit
department’s name?
_____________________________
If you don’t remember these tables from earlier, search the Clarity
Compass for the items EPT .2, SER .2, and DEP .2.
For this report, you may use inner joins. All of the tables are extracted daily and
there should be matching data in the destination tables.
□ 6. Finish building your FROM clause using the following generic structure:
FROM [table 1]
INNER JOIN [table 2]
ON [table 1.foreign key] = [table 2.column]
INNER JOIN [table 3]
ON [table 1.foreign key] = [table 3.column]
INNER JOIN [table 4]
ON [table 1.foreign key] = [table 4.column]
□ 7. Now create your SELECT list. Your results grid should display each of
the following attributes:
Encounter CSN
Encounter Department Name
Encounter Patient Name
Visit Provider Name
Open/Closed Encounter Flag
Encounter Type (Note: if you would like to display the name of the
encounter type, you will need to join to an additional table)
□ 8. Finally, include the required filtering in your query. You will need to
include a WHERE clause.
Filter on the Open/Closed Encounter Flag, including all rows
where that flag contains an ‘N’.
Also filter on the encounter type column; you’ll need to filter to
only the category value that corresponds to “office visit.”
The ON statement in a SQL join can include any logical expression. Whenever
the expression is true for any pair of rows, those two rows will be combined in
your result grid.
You are creating a report on In Basket messages. In Basket messages can
be sent to organization staff as well as patients. You want to link an In
Basket message to the patient or staff member who received it.
The table IB_RECEIVER stores who received each In Basket message. The
column IB_RECEIVER.RECIPIENT stores the ID of the recipient. This is a
patient (EPT) ID if the message was sent to a patient, or a staff (EMP) ID if the
message was sent to staff. This column is used to link to PATIENT and
CLARITY_EMP. Consider the following query:
SELECT
*
FROM IB_RECEIVER ibr
LEFT OUTER JOIN PATIENT pat
ON ibr.RECIPIENT = pat.PAT_ID
LEFT OUTER JOIN CLARITY_EMP emp
ON ibr.RECIPIENT = emp.USER_ID
This query would return results, but some would be inaccurate. Each value in
IB_RECEIVER.RECIPIENT will be treated as both a patient ID and staff ID. If a
patient and staff member happened to have the same ID, both would be returned.
There is another column in IB_RECEIVER which can solve this problem. If
IB_RECEIVER.REGISTRY_ID = 8, the recipient was a patient. If
IB_RECEIVER.REGISTRY_ID = 1, the recipient was a staff member. This query
would be appropriate:
SELECT
*
FROM IB_RECEIVER ibr
LEFT OUTER JOIN PATIENT pat
ON
(
ibr.RECIPIENT = pat.PAT_ID
and ibr.REGISTRY_ID = 8 --recipient is a patient
)
LEFT OUTER JOIN CLARITY_EMP emp
ON
(
ibr.RECIPIENT = emp.USER_ID
and ibr.REGISTRY_ID = 1 --recipient is a staff member
)
)
WHERE
meas.FLO_MEAS_ID in ('6','7')
ORDER BY meas.FSD_ID,meas.RECORDED_TIME
Revenue/Access
You are creating a report on Hospital Accounts. In Resolute Hospital Billing, you
are unable to send bills or claims for hospital accounts that have Stop Bills or
DNB checks placed on them.
The table that stores the history of both Stop Bills and DNB checks is called
HSP_ACCT_DNB_SB_HX. The column in this table that stores Stop Bill reason
or DNB check ID is HSP_ACCT_DNB_SB_HX.HX_DNB_SB.
Depending on whether one row in the table corresponds to a Stop Bill or DNB
check, you will have to link out to one of two places. The column
HSP_ACCT_DNB_SB_HX.HX_DNB_SB will link to
ZC_STOPBILL_RSN_HA.STOPBILL_RSN_HA_C for Stop Bill rows, and
CLARITY_LPP.LPP_ID for DNB check rows.
A row in this table is a Stop Bill row if
HSP_ACCT_DNB_SB_HX.HX_DNB_SB_C = 2. If
HSP_ACCT_DNB_SB_HX.HX_DNB_SB_C = 1, the row is a DNB check.
You want to write a query to show the names of the Stop Bill reasons or names of
DNB checks for accounts with either. Write the ON statements for the two joins
in this query:
SELECT
hx.HSP_ACCOUNT_ID
,hx.HX_DNB_SB_ADD_INST
,stopbill.NAME "Stop Bill Reason"
,dnb.LPP_NAME "DNB Check Name"
FROM HSP_ACCT_DNB_SB_HX hx
--Link to get Stop Bill reason
LEFT OUTER JOIN ZC_STOPBILL_RSN_HA stopbill
ON
(
)
--Link to get DNB check name
LEFT OUTER JOIN CLARITY_LPP dnb
ON
(
)
WHERE
hx.HX_DNB_SB_C IN (1,2)
The results of the above query would look like the following screenshot.
The above example could be altered further to combine the two reasons
into a single column, by altering the rows:
,stopbill.NAME "Stopbill Reason"
,dnb.LPP_NAME "DNB Check Name"
and replacing them with either a CASE:
,CASE hx.HX_DNB_SB_C
WHEN 2
THEN stopbill.NAME
WHEN 1
THEN dnb.LPP_NAME
END AS "Reason"
or, since one is populated when the other is null, a COALESCE() function:
,COALESCE(stopbill.NAME,dnb.NAME) "Reason"
Appending Tables
Occasionally you may want to query two tables together as one. In SQL you are
able to append two tables together using UNION or UNION ALL commands. A
UNION can allow you to quickly combine two separate groups of results.
The UNION command comes between the two queries you would like to append.
For example:
SELECT [columns]
FROM [table 1]
WHERE [condition]
UNION
SELECT [columns]
FROM [table 2]
WHERE [condition]
Imagine you wanted to combine the patients in your system with a last
name of ‘Smith’ to those patients with a last name of ‘Baker.’
To begin, you could break this into two separate queries.
SELECT
pat.PAT_NAME
,pat.BIRTH_DATE
FROM PATIENT pat
WHERE pat.PAT_NAME like 'Smith,%'
SELECT
pat.PAT_NAME
,pat.BIRTH_DATE
FROM PATIENT pat
WHERE pat.PAT_NAME like 'Baker,%'
When these queries are run, you see the two results in separate windows.
If you wanted these results to be together in one table you could add the UNION
command between the two queries. Running this query would return the same
results, combined together in one result set.
Appending the results of two queries is particularly useful if you are trying to
compare two populations.
Imagine you wanted to compare the number of encounters that have
happened at your organization so far this year to the total number from
last year.
To begin, you could break this into two separate queries.
To determine how many encounters have happened in the current year, you
could use the following query:
SELECT 'This Year (in progress)'
,COUNT(*)"Number of Encounters"
FROM PAT_ENC
WHERE
YEAR(CONTACT_DATE) > (YEAR(CURRENT_TIMESTAMP)-1)
Which would return
To find the number of encounters from the previous year, you could use:
SELECT 'Last Year (total)'
,COUNT(*) "Number of Encounters"
FROM PAT_ENC
WHERE
YEAR(CONTACT_DATE) = (YEAR(CURRENT_TIMESTAMP)-1)
Which would return
If you wanted to display these two rows together, you could do so using the
UNION command, placed between the two queries.
SELECT 'This Year (in progress)'
,COUNT(*) "Number of Encounters"
FROM PAT_ENC
WHERE
YEAR(CONTACT_DATE) > (YEAR(CURRENT_TIMESTAMP)-1)
UNION
Now that the two queries have a UNION command between them, you see the
previous two rows together in one set of results:
Not all results can be appended using the UNION command. There are a few
requirements of the datasets in order for UNION to function properly.
Each SELECT statement within the UNION must have the same number of
columns.
The columns of each SELECT statement within the UNION must have
similar data types.
The columns of each SELECT statement within the UNION must be in the
same order.
The UNION command returns only distinct values by default. To allow
duplicate values, use the ALL keyword with UNION.
To view the data type of a column used in your query in SQL Management
Studio, place your mouse over the column name in your query and the
tooltip that appears will give you this information in the format:
COLUMN_NAME ([Primary key], [Data type (length)], [Allow nulls])
The table CLARITY_SA extracts data from the EAF master file, and it will
contain one row for your facility, then an additional row for each service area.
The table CLARITY_DEP extracts data from the DEP master file, and it will
contain one row for each department at your organization. There should be no
repeated records between these two tables, so if you were to append queries from
these two tables you could reasonably expect to use UNION ALL for this.
To verify this, let’s start by creating the two queries you will UNION together.
Execute the two queries within one window. The results you receive should look
similar to what is shown below.
Try adding a UNION command between the two queries. Now update it to
UNION ALL. You should find the number of results did not change, since there
are no rows in the top query which are exactly the same as in the bottom query.
After-Class Exercises
Based on the SQL query above, what joins are used by the Provider
Access Report by Department report?
Review Questions
2. True or False: The UNION command can be used to append any two
tables together.
Review Key
1. True or False: In a SQL query, if you want to include multiple database
objects, one way would be to specify how they will be joined together
in the FROM clause of your query.
True
2. True or False: The UNION command can be used to append any two
tables together.
False. The two database objects need to have the same number of
columns with the same data types in the same order.
Exercise Answers
See the Cogito’s Introduction to Epic’s Certification Environments appendix to
access the answer key files.
Study Checklist
Make sure you can define the following key terms:
Join
Foreign Key
Append
Make sure you can perform the following tasks:
Join multiple database objects in the FROM clause of a SQL query
Use multiple join criteria in the FROM clause of a SQL query
Append multiple database objects using the UNION command
Make sure you fully understand and can explain the following concepts:
Determine when multiple join criteria are needed
Identify when to use the UNION command and the UNION ALL
commands
A Command Object gives you more control over what actions are
performed on the server. It does not automatically make things faster.
Whatever is not done on the server will be performed locally by Crystal
Reports. A poorly written Command Object will likely result in worse
performance then having Crystal generate the SQL statement for you.
□ 13. Use the Group Expert to group the report by Department and by
Admitting Provider.
□ 14. Add the PAT_NAME, HOSP_ADMSN_TIME, HOSP_DISCH_TIME,
and Length of Stay fields to the details section of the report.
□ 15. Format as desired.
Prompting Text: Enter a starting date for your admission date range:
Value Type: Date
Default Value: 1/1/2016
□ 6. Create an EndDate parameter with the following values:
Parameter Name: EndDate
Prompting Text: Enter an ending date for your admission date range:
Value Type: Date
Default Value: 1/1/2017
Do not close the command object window at this point!
If you leave the command object window without using the parameters,
the parameters will be deleted.
Click OK.
□ 8. The Enter Values screen should appear. Notice that the parameters don’t
have any defaults in them yet. The defaults that you added are in the
dropdown menu. Select your default values.
At this point, your parameters appear in reverse order. You'll correct this
later on.
□ 4. You also need to display appointment date and time, as well as order the
results by that field. You’ll need to add the APPT_DTTM column to the
SELECT list, as well as add an ORDER BY clause:
SELECT
appt.APPT_DTTM
,COUNT(*) OVER
(PARTITION BY appt.DEPARTMENT_ID) AS dep_cnt
FROM
F_SCHED_APPT appt
WHERE
appt.CONTACT_DATE between '1/1/2012' and '1/1/2017'
ORDER BY
appt.DEPARTMENT_ID
,appt.APPT_DTTM
□ 5. You need to display patient name and department name. These columns
may be in F_SCHED_APPT. What tools could you use to look these up?
□ 6. Use one of these tools to determine the other two tables you’ll have to
include in your query and the columns on which you’ll need to link them.
Tables you’ll need:
F_SCHED_APPT, _____________________, ____________________
Add these additional two tables to your FROM clause, including the proper linking.
Your query should now look like:
SELECT
appt.APPT_DTTM
,COUNT(*) OVER
(PARTITION BY appt.DEPARTMENT_ID) AS dep_cnt
FROM
F_SCHED_APPT appt
INNER JOIN PATIENT pat
ON appt.PAT_ID = pat.PAT_ID
LEFT OUTER JOIN CLARITY_DEP dep
ON appt.DEPARTMENT_ID = dep.DEPARTMENT_ID
WHERE
appt.CONTACT_DATE between '1/1/2012' and '1/1/2017'
ORDER BY
appt.DEPARTMENT_ID
,appt.APPT_DTTM
□ 7. Add the rest of the necessary display information to your Select
statement.
SELECT
pat.PAT_NAME
,dep.DEPARTMENT_NAME
,appt.APPT_DTTM
,COUNT(*) OVER
(PARTITION BY appt.DEPARTMENT_ID) AS dep_cnt
FROM
F_SCHED_APPT appt
INNER JOIN PATIENT pat
ON appt.PAT_ID = pat.PAT_ID
INNER JOIN CLARITY_DEP dep
ON appt.DEPARTMENT_ID = dep.DEPARTMENT_ID
WHERE
appt.CONTACT_DATE between '1/1/2005' and '1/1/2017'
ORDER BY
appt.DEPARTMENT_ID
,appt.APPT_DTTM
□ 14. Paste your query into the Add Command to Report window:
Review Questions
Review Key
1. What feature can be added to a SQL query using a command object
from Crystal Reports?
Parameters
True
Study Checklist
Make sure you can define the following key terms:
Command Object
Parameter
Make sure you can perform the following tasks:
Add SQL to a Crystal Report using a command object
Using Subqueries
A SQL query acts on one or more tables and also returns results as a table.
Therefore, it is possible to create a query that is nested within another query. The
results of this ‘sub’ query comprise table within the outer ‘parent’ query. In this
chapter, you’ll explore the use of subqueries and how their placement determines
how they are able to be used within a SQL query.
Subqueries
A subquery is a query contained within another query. Subqueries can be used in
any of the following clauses:
SELECT
WHERE
FROM
A subquery is always enclosed within parentheses, and it is usually executed prior
to the entire statement containing the subquery. Subqueries can be classified in
one of two ways.
Type Description
Correlated Refers to columns in the outer ‘parent’ query. Runs once
per line.
Uncorrelated Executed once. The same result is used inside the parent
query multiple times.
Since correlated subqueries are executed once for each row in the parent,
they have the potential to negatively impact performance.
This would also give you the freedom to display any other columns from the
PATIENT table, such as the patient’s address or phone number. To do so, you
would include those columns in the SELECT clause.
But if you only need patient name from the PATIENT table, you can build this
directly within the SELECT list, without including PATIENT in the FROM clause.
A subquery in the SELECT clause has the following basic formula:
SELECT [column]
,(SELECT [data]
FROM [table 2]
WHERE [logic])
FROM [table 1]
If you were to apply this formula to your query, you could continue to display
appointment information and the patient’s name while only listing the
F_SCHED_APPT table in the FROM clause.
Let’s work through building this query.
What you’d like is a third column added to your results, displaying the
patient’s name. Build that into your query, so it now looks like:
SELECT appt.PAT_ENC_CSN_ID
,appt.CONTACT_DATE
,(SELECT PAT_NAME
FROM PATIENT pat) 'Patient Name'
FROM F_SCHED_APPT appt
Did the above query work? Why do you think this is?
____________________________________________________________
You likely received a ‘Subquery returned more than one value’ error message. To
return only one value, you need to limit what is being returned by the subquery.
Rather than listing every patient name that exists in the PATIENT table, you only
want the name of the patient for whom the appointment was conducted.
Correlated Subquery
SELECT
appt.PAT_ENC_CSN_ID
,appt.PAT_ID
,appt.CONTACT_DATE
,(
SELECT
MAX(CONTACT_DATE)
FROM
F_SCHED_APPT prevappt
WHERE
prevappt.PAT_ID = appt.PAT_ID
AND prevappt.CONTACT_DATE >=
DATEADD(year, -1, appt.CONTACT_DATE)
AND prevappt.CONTACT_DATE < appt.CONTACT_DATE
AND prevappt.APPT_STATUS_C = 2
) most_recent_appt_date
FROM
F_SCHED_APPT appt
GROUP BY
appt.PAT_ENC_CSN_ID
,appt.PAT_ID
,appt.CONTACT_DATE
FROM
F_SCHED_APPT appt
WHERE
YEAR(appt.CONTACT_DATE) =
(
SELECT YEAR(MAX(last_appt.CONTACT_DATE))
FROM F_SCHED_APPT last_appt
)
Total encounters per department. Note: the data in the training database may
have changed since the publication of this companion.
Number of encounters by department and encounter type. Note: the data in the
training database may have changed since the publication of this companion.
Calculate percentage
The last step is to combine the above two calculations into a percentage. You will
need the TotalEncs column and NumEncsByType column to calculate your
percentage. In short, you will want to calculate:
To have both columns available in one query, you will need to include the two
result matrices in the FROM clause, which would allow you to use each as though
it were a Clarity table.
Create a new query and copy-paste the two queries you created earlier into it.
Your query window will look similar to what is shown below:
--Query to calculate number of encounters by department
SELECT DEPARTMENT_ID
,COUNT(*) TotalEncs
FROM PAT_ENC
GROUP BY DEPARTMENT_ID
Run your query. You will have both result matrices returned separately. To
bring these together, you will need to include these two queries as subqueries
within an outer, parent query.
Build a parent query around your two subqueries by adding SELECT * and a
FROM clause before your two subqueries. Enclose your two subqueries in
parentheses and give each an alias, similar to below.
SELECT *
FROM
(
--Query to calculate number of encounters by department
SELECT DEPARTMENT_ID
,COUNT(*) TotalEncs
FROM PAT_ENC
GROUP BY DEPARTMENT_ID
) NumEncsByDept
(
--Query to calculate number of encounters by department and
type
SELECT DEPARTMENT_ID
,ENC_TYPE_C Type
,COUNT(ENC_TYPE_C) NumEncsByType
FROM PAT_ENC
GROUP BY PAT_ENC.DEPARTMENT_ID,ENC_TYPE_C
) NumEncsByDeptType
Attempt to run your query. You are presented with an error message. What is
missing from your query?
_____________________________________________________
You need to join your two queries, as though they were tables themselves.
What column do both queries have in common that you could use in your join
expression?
_____________________________________________________
Link your two subqueries together, using a LEFT OUTER JOIN and linking
on the column above. Now run your query.
Troubleshooting tip: you can highlight each subquery within your code
and execute that selection alone to verify that it returns what's desired.
You are presented with all of the columns from both subqueries together in
one result matrix. To calculate the percentage, you will need to add the
calculation as a separate column. Remember, the percentage is calculated as:
Add the columns in the screenshot below to your SELECT clause, then
ORDER BY the department ID. The first few rows of output from your query
should be similar to the result matrix below.
Scope
A query can only access data from the table(s) in its FROM clause. A parent query,
then, can only access the columns in its subqueries’ tables if the subqueries list
those columns in their SELECT clauses. For example, what would happen if you
were to build the following query in SQL Management Studio?
SELECT PAT_NAME
FROM
(
SELECT PAT_ID
,BIRTH_DATE
FROM PATIENT
) patinfo
The above query will return an error. Although PAT_NAME is a column that
exists in the PATIENT table, your subquery is not passing that column to the
outer parent query via its SELECT clause. The parent query only has access to the
PAT_ID and BIRTH_DATE columns. If you did need the PAT_NAME, you
would need to include it in the subquery’s SELECT list.
SELECT PAT_NAME
FROM
(
SELECT PAT_ID
,BIRTH_DATE
,PAT_NAME
FROM PATIENT
) patinfo
SELECT *
FROM
(
SELECT p.CITY
,avg(datediff(d,p.BIRTH_DATE,getdate())) "ageavg"
,count(*) "count"
FROM PATIENT p
GROUP BY p.CITY
) as agecity
□ 7. Run your query. You now have the aggregate data you need, but you
don’t have any patient-level information. You need to display each
patient’s name and ID. Which table stores that information?
______________________________________________
□ 8. When you have a subquery in the FROM clause, you are allowed to join it
to other tables. What column in your subquery is also in the above table?
______________________________________________
□ 9. Use this column to INNER JOIN to the table you identified. Your FROM
clause should look like this:
FROM
(
SELECT p.CITY
,avg(datediff(d,p.BIRTH_DATE,getdate())) "ageavg"
,count(*) "count"
FROM PATIENT p
GROUP BY p.CITY
) as agecity
INNER JOIN PATIENT pat
ON agecity.CITY = pat.CITY
□ 10. Add a WHERE clause to the end of your query so you don’t get cities with
only one patient. You can use the "count" column from your subquery.
□ 11. Now, you need to build your select statement. SELECT the patient ID, the
patient name, and the patient city.
□ 12. For the older/younger than average flag, you need a CASE statement.
You can copy this CASE statement as a new column into your query:
CASE
WHEN datediff(d,pat.BIRTH_DATE,getdate()) < agecity.ageavg
THEN 'Younger'
WHEN datediff(d,pat.BIRTH_DATE,getdate()) > agecity.ageavg
THEN 'Older'
ELSE 'AVERAGE'
END "Compared to average"
SELECT [column]
,[data]
FROM [table 1]
JOIN [alias]
ON [table 1].[column]=[alias].[data]
During a previous activity, you created a query that returns the breakdown of
encounter types by department in terms of percentages (for example, for a
given department what percentage of its encounters are Office Visits versus
Telephone Calls). You created the query using two subqueries. You could
have created this query instead using two common table expressions, as
shown below, to achieve a simpler outer parent query:
WITH NumEncsByDept AS --CTE to calculate number of
encounters by department
(
SELECT DEPARTMENT_ID
,COUNT(*) TotalEncs
FROM PAT_ENC
GROUP BY DEPARTMENT_ID
)
,NumEncsByDeptType AS --CTE to calculate number of
encounters by department and type
(
SELECT DEPARTMENT_ID
,ENC_TYPE_C Type
,COUNT(ENC_TYPE_C) NumEncsByType
FROM PAT_ENC
GROUP BY PAT_ENC.DEPARTMENT_ID,ENC_TYPE_C
)
--Parent query
SELECT NumEncsByDept.DEPARTMENT_ID
,NumEncsByDeptType.Type
,NumEncsByDeptType.NumEncsByType
,NumEncsByDept.TotalEncs
,100*(NumEncsByDeptType.NumEncsByType)/(NumEncsByDept.TotalEn
cs) Percentage
FROM NumEncsByDept
LEFT OUTER JOIN NumEncsByDeptType
ON
NumEncsByDept.DEPARTMENT_ID=NumEncsByDeptType.DEPARTMENT_ID
If you have a query that is having performance issues, one thing to check
is whether the query includes advanced common table expressions. In the
case of complex CTEs, occasionally the Query Optimizer is unable to
handle them efficiently.
Review Questions
3. True or False: A query can only contain one common table expression
(CTE).
Review Key
1. What clauses in SQL can contain a subquery?
You need to join them together, as you would tables in the FROM clause.
3. True or False: A query can only contain one common table expression
(CTE).
Exercise Answers
See the Cogito’s Introduction to Epic’s Certification Environments appendix to
access the answer key files.
Study Checklist
Make sure you can define the following key terms:
Subquery
Scope
Common Table Expression
Make sure you can perform the following tasks:
Include a subquery in the SELECT, WHERE, and FROM clauses
Include a common table expression in a SQL query
Make sure you fully understand and can explain the following concepts:
When to use a subquery
The different effects of including a subquery in the SELECT, WHERE,
and FROM clauses
Capstone Exercise
In this Capstone Exercise you will have a chance to apply your knowledge from
CLR110 Clarity Data Model Fundamentals, including topics such as Finding Data
in Chronicles and Integrating a Crystal Report in to Hyperspace, in order to write
a report about Epic-Crystal Templates in SQL.
On the following page, there is a report request from an end user. Since you
learned in CLR110 how to create an Epic-Crystal Template, you can follow the
proper workflows to find items to CTRL + Left Click or search for known values
in the Record Viewer. Use tools like the Clarity Compass to 1) map the INI and
Item Numbers you find to Tables and Columns in Clarity, and 2) figure out how
the tables you need are related to each other, in order to write a SQL query to
satisfy the report request.
Across Down
1. Epic's umbrella term for 2. (False AND True) OR (False OR
reporting and analytics True)
5. SQL clause for filtering 3. CASE WHEN ____ ELSE END
7. Sorting column values to 4. This database object is a
speed up queries query that is stored in the
8. First clause processed database and does not actually
in a SQL query store results
9. Keyword that only 6. Not Unknown
returns unique column 10. SQL uses _____ Value Logic
values 11. The output of a query
14. Character that selects 12. "String" in a relational
all the columns in a table database
15. /* This */ 13. Keyword to limit our results
16. The first word in a to a number of rows or a
query percentage of total rows