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