DATETIME FUNCTIONS

Pain and joy: two sentiments I associate with handling dates and times using SQL. The pain is born out of the flavors of SQL and associated datetime functions offered by each relational database management system (RDBMS) and the ensuing time spent reviewing the corresponding product documentation. While most RDBMS products will allow you to achieve similar, if not the same thing, when working with dates and times, the function(s) used and/or strategy may differ. The joy is associated with the ease in which analyses requiring temporal dimensions can be done after enduring the documentation. Luckily, the pain is typically short and the joy, long-lasting.

Let’s explore a handful of Oracle’s datetime functions within the context of solving real-world problems. The schemas, corresponding tables, and data used throughout the examples can be found at livesql.oracle.com.

Please note that the following statements have been executed prior to running the code blocks within each example. These individual statements update the default date and timestamp formats.

Updating Session Parameters

ALTER SESSION
    SET NLS_DATE_FORMAT = 'YYYY-MM-DD';

ALTER SESSION
    SET NLS_TIMESTAMP_FORMAT = 'YYYY-MM-DD HH24.MI.SSXFF';

ALTER SESSION
    SET NLS_TIMESTAMP_TZ_FORMAT = 'YYYY-MM-DD HH24.MI.SSXFF TZR';

Retrieving System Date and Time (Client/session and Server)
Having access to the current date, time, or both, can be extremely useful in solving real-world problems. Fortunately, retrieving these values is straightforward in SQL. Depending on which function is used, the value can represent the client’s (i.e., session) or the server’s date and/or time. Simply use the appropriate function to retrieve the desired value. In the code block below, the current date and time for both client and server, are retrieved. The query’s corresponding result set follows the code block.

SELECT
    CURRENT_DATE AS session_date,
    CURRENT_TIMESTAMP AS session_date_time,
	SYSDATE AS server_date,
    SYSTIMESTAMP AS server_date_time
FROM
	dual;
session_datesession_date_timeserver_dateserver_date_time
2023-06-092023-06-09 11.57.04.381344 US/PACIFIC2023-06-092023-06-09 18.57.04.381342 +00:00

The example above illustrates how to use datetime functions to retrieve the current date and/or time; however, it lacks real-world application. Let’s look at an example applying these functions to solve a realistic task.

Imagine I sell my goods online and notice, while business is better than it’s ever been, there are some customers that do not place subsequent orders after their initial order. In an effort to better understand why, I could gather the full list of those customers, details of the orders placed, and set a time boundary, in terms of days since the first order was placed, that delineates customers possibly returning from those that are likely to never return. Of course, to make use of the list of customers, corresponding order details, and the boundary I’ve set, I’ll need to know how to calculate the amount of time elapsed since each of the customer’s first orders. This can be done by simply taking the difference between the SYSDATE value and the order date value for the corresponding order. When using Oracle, the value returned when the difference between two datetime values is computed is of interval data type. The interval contains the number of days, hours, minutes, seconds, and fractional seconds.

/*
Retrieve a list of customers having only placed a single order.
Subsequently, retrieve those customer's order details and compute 
the amount of time elapsed since the order was placed.
*/
WITH single_order_customer AS (
    SELECT
    	oe.orders.customer_id
    FROM
    	oe.orders
    GROUP BY
    	oe.orders.customer_id
    HAVING
    	COUNT(*) = 1
)
SELECT
	oe.orders.customer_id,
    oe.orders.order_id,
    SYSDATE - oe.orders.order_date AS time_elapsed,
    oe.orders.order_mode,
    oe.orders.order_total,
    oe.orders.sales_rep_id
FROM
	oe.orders
    INNER JOIN
    single_order_customer
    	ON oe.orders.customer_id = single_order_customer.customer_id
ORDER BY
    time_elapsed DESC;
customer_idorder_idtime_elapsedorder_modeorder_totalsales_rep_id
1562395+000006338 14:48:08.772450direct68501163
1532391+000006313 18:04:16.171670direct48070.6156
1702412+000006284 00:45:10.490199direct66816158
1222375+000005950 07:17:29.540767online103834.4 –
1192372+000005949 10:44:46.643211online16447.2 –
1432380+000005871 02:14:17.090910online27132.6 –
1422378+000005863 03:08:09.989899online25691.3 –
1692411+000005863 00:45:09.451361direct15760.5156
1502388+000005851 19:26:07.445565online282694.3 –
1232376+000005849 05:49:11.116690online11006.2 –
1412377+000005849 05:04:18.998900online38017.8 –
1652407+000005827 05:03:58.473995direct2519155
1662408+000005827 03:07:48.666383direct309158
1672409+000005827 02:13:38.015499direct48154
1622403+000005824 19:18:06.384488direct220154
1632404+000005824 19:18:06.335915direct510158
1642405+000005824 19:18:06.321877direct1233159
1612402+000005824 08:32:35.334830direct600154
1592400+000005816 10:32:50.440613direct69286.4161
1602401+000005816 09:44:26.445178direct969.2163
1542392+000005804 15:07:22.428943direct26632161
1522390+000005684 18:48:29.453149online7616.8 –
1572398+000005683 13:44:26.775825direct7110.3163
1582399+000005683 12:44:41.659010direct25270.3161
1552393+000005600 15:14:00.471798direct23431.9161
1202373+000005584 09:32:28.779935online416 –
1212374+000005584 08:25:34.890346online4797 –
1682410+000005497 01:47:28.014499direct45175156
1512389+000005485 18:17:36.453046online17620 –

As a next step, the time boundary could be implemented as a filter within the query to limit your data to only those corresponding to customers with an amount of time since placing their first order exceeding some value (e.g., 45 days). I’ll do just that after exploring the function used to extract portions of the interval values returned from the query above and datetime values, in general.

Extracting Date Parts
Often, reporting and analytics tasks will require data to be aggregated at a higher level of detail than it originally exists. For example, when sales are captured as individual transactions with a transaction date (e.g., 2023/13/06), it may be necessary to aggregate the sales data at the month or year level (i.e., sales by month, sales by year) due to the volume of transactions. Meaningful information may also be gleaned from analyzing sales at a lower level of detail. For example, suppose I own a coffee shop and wish to ensure my shop is adequately staffed at all times. I could look at sales at the hour level to understand when my coffee shop is typically busy and when it’s rather slow.

To easily handle these types of tasks, most RDBMS products provide the function, EXTRACT. This function permits the extracting of specific parts of a value of datetime data type (e.g., date, timestamp, etc.). Below is a simple example highlighting the functionality provided by EXTRACT. In short, for order 2458, the order date is broken down into its individual parts (e.g., year, month, day, etc.). See the result set that follows for clarity.

SELECT
    oe.orders.order_id,
    oe.orders.order_date,
	EXTRACT(YEAR FROM oe.orders.order_date) AS order_year,
	EXTRACT(MONTH FROM oe.orders.order_date) AS order_month,
	EXTRACT(DAY FROM oe.orders.order_date) AS order_day,
	EXTRACT(HOUR FROM oe.orders.order_date) AS order_hour,
	EXTRACT(MINUTE FROM oe.orders.order_date) AS order_minute,
	EXTRACT(SECOND FROM oe.orders.order_date) AS order_second
FROM
	oe.orders
WHERE
    oe.orders.order_id = 2458;
order_idorder_dateorder_yearorder_monthorder_dayorder_hourorder_minuteorder_second
24582007-08-16 15.34.12.2343592007816153412.234359

While the example above tackles extracting the parts of a datetime value and representing each of those parts as an individual column, I find it more common to use specific date parts for aggregation purposes. As previously mentioned, sales could be aggregated at the year level. The example below retrieves all orders and groups them by year, returning the order year and total sales value corresponding to each year’s orders. The same code could easily be adjusted to calculate sales at a different level of detail by adjusting the date part used in the EXTRACT function.

SELECT
	EXTRACT(YEAR FROM oe.orders.order_date) AS order_year,
    SUM(oe.orders.order_total) AS total_sales
FROM
	oe.orders
GROUP BY
	EXTRACT(YEAR FROM oe.orders.order_date)
ORDER BY
	order_year;
order_yeartotal_sales
20045546.6
2006471952.1
20072545098.3
2008645457.7

Truncating Datetime Values
In the previous section, EXTRACT was used to pull out specific parts of a datetime value and to represent those parts in individual columns. When EXTRACT is used, it returns the numeric representation of the specified part. That’s typically the desired result; however, there are instances where it’s useful to maintain the datetime data type of the original value. For example, recall the query above returning sales by year. This example can be extended to capturing sales by year and month by simply adding an additional column containing the extracted month value and updating the GROUP BY clause to reflect the newly added column. While the result set does technically contain sales by year and month, year and month values are spread across two columns. If the goal was to achieve the same result set, but with the year and month values represented in a single column, then there are a couple of options that come to mind. Concatenate the extraction of year and month values along with a constant placeholder for the first of the month or truncate the original datetime values to the month portion using the truncate function, TRUNC. I’ll focus on the latter.

TRUNC allows a user to represent a datetime value with a specified portion of the value truncated. In the example below, the server’s date and time value is captured (i.e. SYSDATE). Additionally, this same value is used as an argument to the TRUNC function and the desired unit or portion of the date to truncate to is specified. Passing the year unit to this function will return the date value corresponding to the first of the year for the date value passed. As seen in the example’s corresponding result set, using the date, 2023-06-17, and specifying to truncate to the year unit yields 2023-01-01. Similarly, when specifying to truncate to the month unit, the function will return the date value corresponding to the first of the month for the date value passed (i.e., 2023-06-01). Truncating to the month unit can be useful when it’s necessary to aggregate a measure by year and month. Again, think about the previous example where the EXTRACT function was used to represent these date parts across two columns.

In the fourth column of the example code below, notice the absence of a date part or unit argument for the TRUNC function. When a unit is not provided, the default value of DAY is used. Truncating to the day portion of the date value passed will strip the value of its time parts. For clarity, review the final three columns in the code example below and the corresponding result set.

SELECT
    SYSDATE,
	TRUNC(SYSDATE, 'YEAR') AS first_of_the_year,
    TRUNC(SYSDATE, 'MONTH') AS first_of_the_month,
    SYSTIMESTAMP AS server_date_time,
    TRUNC(SYSTIMESTAMP) AS server_date,
    TO_TIMESTAMP(TRUNC(SYSTIMESTAMP)) AS beginning_of_the_day
FROM
	dual;
sysdatefirst_of_the_yearfirst_of_the_monthserver_date_timeserver_datebeginning_of_the_day
2023-06-272023-01-012023-06-012023-06-27 11.12.52.856655 +00:002023-06-272023-06-27 00.00.00.000000

Date Arithmetic
The need for adding and subtracting dates is all but guaranteed in analytics tasks. Questions, such as how long has it been since a particular event, how much time elapsed between two events, and what is the average or median time elapsed between two particular types of events over time are examples of information commonly sought after.

Answering these questions can be straightforward with a modicum of understanding as it relates to each RDBMS handling of date arithmetic. Using Oracle database, a given number of days and a datetime value can be added by simply using the addition operator, “+.” This is because the default interval for date arithmetic is days. For example, if there is a pre-existing column, “order_date,” and the goal is the compute or return the datetime value associated with seven days after the order date value, then the following expression will achieve that.

order_date + 7

Similarly, days can be subtracted from the same pre-existing column by using the subtraction operator, “-.”

When intervals aside from days are needed (e.g., weeks, months, years, etc.), we can take advantage of Oracle’s interval expressions and/or built-in functions. Let’s explore interval expressions and built-in functions by examining a series of real-world problems.

Employee Milestone Dates
In this problem, there exists a table containing employee information. Within this table are various attributes describing the employee; however, for the moment, consider two attributes, employee ID and hire date. The employee ID uniquely identifies an employee while the hire date refers to the date on which the associated employee was hired. The task here is to enrich the data by deriving various new attributes. These attributes are described below.

Benefits eligible date
The date the employee becomes eligible for benefits. This occurs 30 days after the employee is hired.

Initial review data (90-day review)
The date the employee’s initial review takes place. This occurs 90 days after the employee is hired.

One-year anniversary
The date of the employee’s one year anniversary. An employee’s anniversary date falls on the same calendar day each year. The manager should receive a notification of their employee’s upcoming one year anniversary seven days prior.

Five-year anniversary
The date of the employee’s five year anniversary.

The example solution and corresponding result set below highlights one way this task can be achieved. The default interval expression, days, is used to compute the benefits eligibility date and the ninety-day review date for the employee with an ID of 100. The ADD_MONTHS function is used to determine the employee’s one year anniversary date, the date for which their manager should receive a notification of the upcoming anniversary, and the employee’s five-year anniversary date. The function is quite straightforward, accepting two arguments. This first argument is the date to add months to and the second, the number of months to add.

SELECT
    hr.employees.employee_id,
    hr.employees.hire_date,
    hr.employees.hire_date + 30 AS benefits_eligible_date,
    hr.employees.hire_date + 90 AS ninety_day_review,
    ADD_MONTHS(hr.employees.hire_date, 12) AS one_year_anniversary,
    ADD_MONTHS(hr.employees.hire_date, 12) - 7 AS manager_email_notice,
    ADD_MONTHS(hr.employees.hire_date, 60) AS five_year_anniversay
FROM
	hr.employees
WHERE
    hr.employees.employee_id = 100;
employee_idhire_datebenefits_eligible_dateninety_day_reviewone_year_anniversarymanager_email_noticefive_year_anniversay
1002003-06-172003-07-172003-09-152004-06-172004-06-102008-06-17

Although the example above uses the ADD_MONTHS function in deriving the final three attributes, it may make more sense to use interval expressions when the number of months being added is greater than eleven and can be expressed in whole years (e.g., twelve for one, twenty-four for two, sixty for five, etc.). This is because the reader of the code, whether that be someone else or your future self, may be able to understand it more quickly. The following code will yield the same result set above.

SELECT
    hr.employees.employee_id,
    hr.employees.hire_date,
    hr.employees.hire_date + 30 AS benefits_eligible_date,
    hr.employees.hire_date + 90 AS ninety_day_review,
	hr.employees.hire_date + INTERVAL '1' YEAR AS one_year_anniversary,
	hr.employees.hire_date + INTERVAL '1' YEAR - 7 AS manager_email_notice,
	hr.employees.hire_date + INTERVAL '5' YEAR AS five_year_anniversay
FROM
	hr.employees
WHERE
    hr.employees.employee_id = 100;

For the three real-world problems that follow, the orders table below will be used.

order_idorder_dateorder_modecustomer_idorder_statusorder_totalsales_rep_idpromotion_id
245816-AUG-07 03.34.12.234359 PMdirect101078279.6153–
239719-NOV-07 02.41.54.696211 PMdirect102142283.2154–
245402-OCT-07 05.49.34.678340 PMdirect10316653.4154–
235414-JUL-08 06.18.23.234567 PMdirect104046257155–
235808-JAN-08 05.03.12.654278 PMdirect10527826155–
238114-MAY-08 08.59.08.843679 PMdirect106323034.6156–
244031-AUG-07 09.53.06.008765 PMdirect107370576.9156–
235708-JAN-06 08.19.44.123456 PMdirect108559872.4158–
239410-FEB-08 09.22.35.564789 PMdirect109521863158–
243502-SEP-07 11.22.53.134567 PMdirect144662303159–
245520-SEP-07 11.34.11.456789 AMdirect145714087.5160–
237916-MAY-07 02.22.24.234567 AMdirect146817848.2161–
239602-FEB-06 01.34.56.345678 AMdirect147834930161–
240629-JUN-07 04.41.20.098765 AMdirect14882854.2161–
243413-SEP-07 05.49.30.647893 AMdirect1498268651.8161–
243602-SEP-07 06.18.04.378034 AMdirect11686394.8161–
244627-JUL-07 07.03.08.302945 AMdirect1178103679.3161–
244727-JUL-08 08.59.10.223344 AMdirect101833893.6161–
243214-SEP-07 09.53.40.223345 AMdirect1021010523163–
243313-SEP-07 10.19.00.654279 AMdirect1031078163–
235526-JAN-06 09.22.51.962632 AMonline104894513.5––
235626-JAN-08 09.22.41.934562 AMonline105529473.8––
235908-JAN-06 09.34.13.112233 PMonline10695543.1––
236014-NOV-07 12.22.31.223344 PMonline1074990.4––
236113-NOV-07 01.34.21.986210 PMonline1088120131.3––
236213-NOV-07 02.41.10.619477 PMonline109492829.4––
236323-OCT-07 05.49.56.346122 PMonline144010082.3––
236428-AUG-07 06.18.45.942399 PMonline14549500––
236528-AUG-07 07.03.34.003399 PMonline146927455.3––
236628-AUG-07 08.59.23.144778 PMonline147537319.4––
236727-JUN-08 09.53.32.335522 PMonline14810144054.8––
236826-JUN-08 10.19.43.190089 PMonline1491060065––
236926-JUN-07 11.22.54.009932 PMonline116011097.4––
237027-JUN-08 12.22.11.647398 AMonline1174126––
237116-MAY-07 01.34.56.113356 AMonline118679405.6––
237227-FEB-07 12.22.33.356789 AMonline119916447.2––
237327-FEB-08 01.34.51.220065 AMonline1204416––
237427-FEB-08 02.41.45.109654 AMonline12104797––
237526-FEB-07 03.49.50.459233 AMonline1222103834.4––
237607-JUN-07 06.18.08.883310 AMonline123611006.2––
237707-JUN-07 07.03.01.001100 AMonline141538017.8––
237824-MAY-07 08.59.10.010101 AMonline142525691.3––
238016-MAY-07 09.53.02.909090 AMonline143327132.6––
238214-MAY-08 10.19.03.828321 AMonline144871173––
238312-MAY-08 11.22.30.545103 AMonline145836374.7––
238412-MAY-08 12.22.34.525972 PMonline146329249.1––
238508-DEC-07 11.34.11.331392 AMonline1474295892––
238606-DEC-07 12.22.34.225609 PMonline1481021116.9––
238711-MAR-07 03.34.56.536966 PMonline149552758.9––
238804-JUN-07 04.41.12.554435 PMonline1504282694.3––
238904-JUN-08 05.49.43.546954 PMonline151417620––
239018-NOV-07 04.18.50.546851 PMonline15297616.8––
239127-FEB-06 05.03.03.828330 PMdirect153248070.6156–
239221-JUL-07 08.59.57.571057 PMdirect154926632161–
239310-FEB-08 07.53.19.528202 PMdirect155423431.9161–
239502-FEB-06 08.19.11.227550 PMdirect156368501163–
239819-NOV-07 09.22.53.224175 PMdirect15797110.3163–
239919-NOV-07 10.22.38.340990 PMdirect158025270.3161–
240010-JUL-07 01.34.29.559387 AMdirect159269286.4161–
240110-JUL-07 02.22.53.554822 AMdirect1603969.2163–
240202-JUL-07 03.34.44.665170 AMdirect1618600154–
240301-JUL-07 04.49.13.615512 PMdirect1620220154–
240401-JUL-07 04.49.13.664085 PMdirect1636510158–
240501-JUL-07 04.49.13.678123 PMdirect16451233159–
240729-JUN-07 07.03.21.526005 AMdirect16592519155–
240829-JUN-07 08.59.31.333617 AMdirect1661309158–
240929-JUN-07 09.53.41.984501 AMdirect167248154–
241024-MAY-08 10.19.51.985501 AMdirect168645175156–
241124-MAY-07 11.22.10.548639 AMdirect169815760.5156–
241229-MAR-06 10.22.09.509801 AMdirect170966816158–
241329-MAR-08 01.34.04.525934 PMdirect101548552161–
241429-MAR-07 02.22.40.536996 PMdirect102810794.6153–
241529-MAR-06 01.34.50.545196 PMdirect1036310161–
241629-MAR-07 04.41.20.945676 PMdirect1046384160–
241720-MAR-07 05.49.10.974352 PMdirect10551926.6163–
241820-MAR-04 04.18.21.862632 PMdirect10645546.6163–
241920-MAR-07 07.03.32.764632 PMdirect107331574160–
242013-MAR-07 08.59.43.666320 PMdirect108229750160–
242112-MAR-07 09.53.54.562432 PMdirect109172836––
242216-DEC-07 08.19.55.462332 PMdirect144211188.5153–
242321-NOV-07 10.22.33.362632 AMdirect145310367.7160–
242421-NOV-07 10.22.33.263332 AMdirect146413824153–
242516-NOV-06 11.34.22.162552 PMdirect14751500.8163–
242617-NOV-06 12.22.11.262552 AMdirect14867200––
242710-NOV-07 01.34.22.362124 AMdirect14979055163–
242810-NOV-07 02.41.34.463567 AMdirect116814685.8––
242910-NOV-07 03.49.25.526321 AMdirect117950125154–
243002-OCT-07 06.18.36.663332 AMdirect101829669.9159–
243114-SEP-06 07.03.04.763452 AMdirect10215610.6163–
243701-SEP-06 08.59.15.826132 AMdirect103413550163–
243801-SEP-07 09.53.26.934626 AMdirect10405451154–
243931-AUG-07 10.19.37.811132 AMdirect105122150.1159–
244101-AUG-08 11.22.48.734526 AMdirect10652075.2160–
244227-JUL-06 12.22.59.662632 PMdirect107952471.9154–
244327-JUL-06 01.34.16.562632 PMdirect10803646154–
244427-JUL-07 02.22.27.462632 PMdirect109177727.2155–
244527-JUL-06 03.34.38.362632 PMdirect14485537.8158–
244818-JUN-07 04.41.49.262632 PMdirect14551388158–
244913-JUN-07 05.49.07.162632 PMdirect146686155–
245011-APR-07 06.18.10.362632 PMdirect14731636159–
245117-DEC-07 05.03.52.562632 PMdirect148710474.6154–
245206-OCT-07 08.59.43.462632 PMdirect149512589159–
245304-OCT-07 09.53.34.362632 PMdirect1160129153–
245607-NOV-06 07.53.25.989889 PMdirect11703878.4163–
245731-OCT-07 11.22.16.162632 PMdirect118521586.2159–

Computing Days Elapsed
The need to determine the amount of time elapsed since a particular event is common. Imagine your manager requires a report indicating, for each customer, the date of the customer’s most recent order and the number of whole days elapsed since that date.

The following coding example utilizes previous learnings to generate the desired report. The day value is extracted from interval expression resulting from the difference between the current system date and the date associated with the customer’s most recent order.

SELECT
    oe.orders.customer_id,
    MAX(oe.orders.order_date) AS most_recent_order,
    EXTRACT(DAY FROM (SYSDATE - MAX(oe.orders.order_date))) AS days_since_last_order
FROM
	oe.orders
GROUP BY
	oe.orders.customer_id;
customer_idmost_recent_orderdays_since_last_order
1072007-11-14 12.22.31.2233445686
1082007-11-13 13.34.21.9862105687
1582007-11-19 22.22.38.3409905680
1612007-07-02 03.34.44.6651705821
1662007-06-29 08.59.31.3336175824
1052008-01-26 09.22.41.9345625613
1092008-02-10 21.22.35.5647895597
1432007-05-16 09.53.02.9090905868
1592007-07-10 01.34.29.5593875813
1622007-07-01 16.49.13.6155125822
1632007-07-01 16.49.13.6640855822
1682008-05-24 10.19.51.9855015494
1042008-07-14 18.18.23.2345675443
1182007-10-31 23.22.16.1626325699
1452008-05-12 11.22.30.5451035506
1212008-02-27 02.41.45.1096545581
1412007-06-07 07.03.01.0011005846
1552008-02-10 19.53.19.5282025597
1012008-07-27 08.59.10.2233445430
1032007-10-02 17.49.34.6783405729
1162007-11-10 02.41.34.4635675690
1202008-02-27 01.34.51.2200655581
1422007-05-24 08.59.10.0101015860
1512008-06-04 17.49.43.5469545483
1562006-02-02 20.19.11.2275506335
1572007-11-19 21.22.53.2241755680
1692007-05-24 11.22.10.5486395860
1462008-05-12 12.22.34.5259725506
1482008-06-27 21.53.32.3355225459
1492008-06-26 22.19.43.1900895460
1222007-02-26 03.49.50.4592335947
1522007-11-18 16.18.50.5468515682
1702006-03-29 10.22.09.5098016281
1442008-05-14 10.19.03.8283215504
1192007-02-27 00.22.33.3567895946
1532006-02-27 17.03.03.8283306311
1642007-07-01 16.49.13.6781235822
1672007-06-29 09.53.41.9845015824
1022007-11-19 14.41.54.6962115681
1062008-08-01 11.22.48.7345265425
1472007-12-08 11.34.11.3313925662
1172008-06-27 00.22.11.6473985460
1232007-06-07 06.18.08.8833105846
1502007-06-04 16.41.12.5544355849
1542007-07-21 20.59.57.5710575801
1602007-07-10 02.22.53.5548225813
1652007-06-29 07.03.21.5260055824

Computing Months Elapsed
Like the task above, the need is to compute the amount of time elapsed. However, for this task, the number of months elapsed between each employee’s hire date and their manager’s hire date is of interest. Have managers been with the company longer than their direct reports or are there employees reporting to managers with less time at the company? How much more or less time have managers been at the company compared to each of their employees? Being able to answer those two questions is the initial goal.

In the example code below, the MONTHS_BETWEEN function is used. What does the function do? You guessed it, it calculates the number of months between two datetime values provided. The ROUND function is wrapped around the MONTHS_BETWEEN function to round the number of months elapsed to two decimal places. More information on the ROUND function and other numeric functions, with real-world examples, can be found on my numeric functions page.

Please note only the first five rows are shown in the result set.

SELECT
    employee.employee_id,
    manager.employee_id AS manager_id,
    employee.hire_date AS employee_hire_date,
    manager.hire_date AS manager_hire_date,
    ROUND(
    	MONTHS_BETWEEN(
        	manager.hire_date,
        	employee.hire_date
    	),
    	2
    ) AS months_elapsed
FROM
	hr.employees employee
    LEFT JOIN
    hr.employees manager
    	ON employee.manager_id = manager.employee_id
FETCH FIRST 5 ROWS ONLY;
employee_idmanager_idemployee_hire_datemanager_hire_datemonths_elapsed
1011002005-09-212003-06-17-27.13
1021002001-01-132003-06-1729.13
1141002002-12-072003-06-176.32
1201002004-07-182003-06-17-13.03
1211002005-04-102003-06-17-21.77

Retrieving the Last Day and Second of the Month
In this final problem, imagine a company guarantees every order placed by customers to be shipped in the same month as the month the order is placed. As a result, management considers proper order tracking crucial to maintaining this promise to its customers. A report outlining each order, the ship by date and time, and the amount of time remaining until the order must be shipped is required.

From an SQL perspective, this problem boils down to being able to derive the datetime value associated with the last second of the last day of each order’s order month. With that value for each order, the difference between it and each order’s order date can be computed to determine the amount of time remaining for the order to be shipped.

Many of the concepts previously covered are incorporated in the example code below. Focusing on the time remaining attribute, the LAST_DAY function is used to determine the last day of each order’s order month. This value is then truncated to the day portion, stripping away the hours, minutes, seconds, and fractional seconds date parts. This ensures when a day is added to the resulting truncated date the first of the following month is returned. Subtracting one second from the first of the month for the month following each order’s order date returns the last second of the month for which each order was placed. Finally, the difference between the datetime value associated with the last second in each order’s order month and the original order date value is computed. This difference is returned as an interval value. In the next section, I’ll revisit how to extract each of the resulting interval’s parts with a fun twist, but for now, see the result set following the example code to make sense of the corresponding output. The ship by date and ship by date and time have been included in the output for clarity.

Please note only the first five rows are shown in the result set.

SELECT
	oe.orders.order_id,
    oe.orders.order_date,
    LAST_DAY(oe.orders.order_date) AS ship_by_date,
	TO_TIMESTAMP(
        (
        	TRUNC(LAST_DAY(oe.orders.order_date))
        	+ 1
        	- INTERVAL '1' SECOND
        )
    ) AS ship_by_date_time,
    (
    	TRUNC(LAST_DAY(oe.orders.order_date))
    	+ 1
    	- INTERVAL '1' SECOND
    	-  oe.orders.order_date
    ) AS time_remaining
FROM
	oe.orders
FETCH FIRST 5 ROWS ONLY;
order_idorder_dateship_by_dateship_by_datetimetime_remaining
24582007-08-16 15.34.12.2343592007-08-312007-08-31 00.00.00.000000+000000015 08:25:46.765641
23972007-11-19 14.41.54.6962112007-11-302007-11-30 00.00.00.000000+000000011 09:18:04.303789
24542007-10-02 17.49.34.6783402007-10-312007-10-31 00.00.00.000000+000000029 06:10:24.321660
23542008-07-14 18.18.23.2345672008-07-312008-07-31 00.00.00.000000+000000017 05:41:35.765433
23582008-01-08 17.03.12.6542782008-01-312008-01-31 00.00.00.000000+000000023 06:56:46.345722

Intervals as a Plain English String
In the Extracting Date Parts section of this page, I covered Oracle’s EXTRACT function with a couple of examples. In each of the examples, the focus was on extracting a portion or part of a datetime value and representing it as a new attribute. But what if we want to translate interval values, such as those found within the time remaining attribute in the output above, to plain English? The EXTRACT function combined with somewhat clever concatenation can achieve this while maintaining the values in a single column.

I’ve taken the example code written above and expressed it as a common table expression (CTE) the code below. If you’re unfamiliar with common table expressions, then it may be helpful to read my page containing a high-level overview on the topic, with supporting examples, here. From the shipping expectation CTE, each attribute is selected, and an additional one is derived. This newly derived attribute concatenates the numeric values associated with each date part within the original interval value and the corresponding plain English unit (e.g., days, hours). The resulting string resembles a countdown timer. Neat! See the result set that immediately follows the code for clarity.

WITH shipping_expectation AS (
    SELECT
    	oe.orders.order_id,
        oe.orders.order_date,
        TRUNC(LAST_DAY(oe.orders.order_date)) AS ship_by_date,
    	TO_TIMESTAMP(
            (
            	TRUNC(LAST_DAY(oe.orders.order_date))
            	+ 1
            	- INTERVAL '1' SECOND
            )
        ) AS ship_by_date_time,
        (
        	TRUNC(LAST_DAY(oe.orders.order_date))
        	+ 1
        	- INTERVAL '1' SECOND
        	-  oe.orders.order_date
        ) AS time_remaining
    FROM
    	oe.orders
    FETCH FIRST 5 ROWS ONLY
)
SELECT
    shipping_expectation.order_id,
    shipping_expectation.order_date,
    shipping_expectation.ship_by_date,
    shipping_expectation.ship_by_date_time,
    shipping_expectation.time_remaining,
	(
    	CAST(EXTRACT(DAY FROM shipping_expectation.time_remaining) AS VARCHAR2(255))
    	|| ' days, '
    	|| CAST(EXTRACT(HOUR FROM shipping_expectation.time_remaining) AS VARCHAR2(2))
    	|| ' hours, '
    	|| CAST(EXTRACT(MINUTE FROM shipping_expectation.time_remaining) AS VARCHAR2(2))
    	|| ' minutes, '
    	|| CAST(EXTRACT(SECOND FROM shipping_expectation.time_remaining) AS VARCHAR2(2))
    	|| ' seconds'
    ) AS time_remaining_string
FROM
    shipping_expectation;
order_idorder_dateship_by_dateship_by_datetimetime_remainingtime_remaining_string
24582007-08-16 15.34.12.2343592007-08-312007-08-31 00.00.00.000000+000000015 08:25:46.76564115 days, 8 hours, 25 minutes, 47 seconds
23972007-11-19 14.41.54.6962112007-11-302007-11-30 00.00.00.000000+000000011 09:18:04.30378911 days, 9 hours, 18 minutes, 4 seconds
24542007-10-02 17.49.34.6783402007-10-312007-10-31 00.00.00.000000+000000029 06:10:24.32166029 days, 6 hours, 10 minutes, 24 seconds
23542008-07-14 18.18.23.2345672008-07-312008-07-31 00.00.00.000000+000000017 05:41:35.76543317 days, 5 hours, 41 minutes, 36 seconds
23582008-01-08 17.03.12.6542782008-01-312008-01-31 00.00.00.000000+000000023 06:56:46.34572223 days, 6 hours, 56 minutes, 46 seconds