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_date | session_date_time | server_date | server_date_time |
|---|---|---|---|
| 2023-06-09 | 2023-06-09 11.57.04.381344 US/PACIFIC | 2023-06-09 | 2023-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_id | order_id | time_elapsed | order_mode | order_total | sales_rep_id |
|---|---|---|---|---|---|
| 156 | 2395 | +000006338 14:48:08.772450 | direct | 68501 | 163 |
| 153 | 2391 | +000006313 18:04:16.171670 | direct | 48070.6 | 156 |
| 170 | 2412 | +000006284 00:45:10.490199 | direct | 66816 | 158 |
| 122 | 2375 | +000005950 07:17:29.540767 | online | 103834.4 | – |
| 119 | 2372 | +000005949 10:44:46.643211 | online | 16447.2 | – |
| 143 | 2380 | +000005871 02:14:17.090910 | online | 27132.6 | – |
| 142 | 2378 | +000005863 03:08:09.989899 | online | 25691.3 | – |
| 169 | 2411 | +000005863 00:45:09.451361 | direct | 15760.5 | 156 |
| 150 | 2388 | +000005851 19:26:07.445565 | online | 282694.3 | – |
| 123 | 2376 | +000005849 05:49:11.116690 | online | 11006.2 | – |
| 141 | 2377 | +000005849 05:04:18.998900 | online | 38017.8 | – |
| 165 | 2407 | +000005827 05:03:58.473995 | direct | 2519 | 155 |
| 166 | 2408 | +000005827 03:07:48.666383 | direct | 309 | 158 |
| 167 | 2409 | +000005827 02:13:38.015499 | direct | 48 | 154 |
| 162 | 2403 | +000005824 19:18:06.384488 | direct | 220 | 154 |
| 163 | 2404 | +000005824 19:18:06.335915 | direct | 510 | 158 |
| 164 | 2405 | +000005824 19:18:06.321877 | direct | 1233 | 159 |
| 161 | 2402 | +000005824 08:32:35.334830 | direct | 600 | 154 |
| 159 | 2400 | +000005816 10:32:50.440613 | direct | 69286.4 | 161 |
| 160 | 2401 | +000005816 09:44:26.445178 | direct | 969.2 | 163 |
| 154 | 2392 | +000005804 15:07:22.428943 | direct | 26632 | 161 |
| 152 | 2390 | +000005684 18:48:29.453149 | online | 7616.8 | – |
| 157 | 2398 | +000005683 13:44:26.775825 | direct | 7110.3 | 163 |
| 158 | 2399 | +000005683 12:44:41.659010 | direct | 25270.3 | 161 |
| 155 | 2393 | +000005600 15:14:00.471798 | direct | 23431.9 | 161 |
| 120 | 2373 | +000005584 09:32:28.779935 | online | 416 | – |
| 121 | 2374 | +000005584 08:25:34.890346 | online | 4797 | – |
| 168 | 2410 | +000005497 01:47:28.014499 | direct | 45175 | 156 |
| 151 | 2389 | +000005485 18:17:36.453046 | online | 17620 | – |
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_id | order_date | order_year | order_month | order_day | order_hour | order_minute | order_second |
|---|---|---|---|---|---|---|---|
| 2458 | 2007-08-16 15.34.12.234359 | 2007 | 8 | 16 | 15 | 34 | 12.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_year | total_sales |
|---|---|
| 2004 | 5546.6 |
| 2006 | 471952.1 |
| 2007 | 2545098.3 |
| 2008 | 645457.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;
| sysdate | first_of_the_year | first_of_the_month | server_date_time | server_date | beginning_of_the_day |
|---|---|---|---|---|---|
| 2023-06-27 | 2023-01-01 | 2023-06-01 | 2023-06-27 11.12.52.856655 +00:00 | 2023-06-27 | 2023-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_id | hire_date | benefits_eligible_date | ninety_day_review | one_year_anniversary | manager_email_notice | five_year_anniversay |
|---|---|---|---|---|---|---|
| 100 | 2003-06-17 | 2003-07-17 | 2003-09-15 | 2004-06-17 | 2004-06-10 | 2008-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_id | order_date | order_mode | customer_id | order_status | order_total | sales_rep_id | promotion_id |
|---|---|---|---|---|---|---|---|
| 2458 | 16-AUG-07 03.34.12.234359 PM | direct | 101 | 0 | 78279.6 | 153 | – |
| 2397 | 19-NOV-07 02.41.54.696211 PM | direct | 102 | 1 | 42283.2 | 154 | – |
| 2454 | 02-OCT-07 05.49.34.678340 PM | direct | 103 | 1 | 6653.4 | 154 | – |
| 2354 | 14-JUL-08 06.18.23.234567 PM | direct | 104 | 0 | 46257 | 155 | – |
| 2358 | 08-JAN-08 05.03.12.654278 PM | direct | 105 | 2 | 7826 | 155 | – |
| 2381 | 14-MAY-08 08.59.08.843679 PM | direct | 106 | 3 | 23034.6 | 156 | – |
| 2440 | 31-AUG-07 09.53.06.008765 PM | direct | 107 | 3 | 70576.9 | 156 | – |
| 2357 | 08-JAN-06 08.19.44.123456 PM | direct | 108 | 5 | 59872.4 | 158 | – |
| 2394 | 10-FEB-08 09.22.35.564789 PM | direct | 109 | 5 | 21863 | 158 | – |
| 2435 | 02-SEP-07 11.22.53.134567 PM | direct | 144 | 6 | 62303 | 159 | – |
| 2455 | 20-SEP-07 11.34.11.456789 AM | direct | 145 | 7 | 14087.5 | 160 | – |
| 2379 | 16-MAY-07 02.22.24.234567 AM | direct | 146 | 8 | 17848.2 | 161 | – |
| 2396 | 02-FEB-06 01.34.56.345678 AM | direct | 147 | 8 | 34930 | 161 | – |
| 2406 | 29-JUN-07 04.41.20.098765 AM | direct | 148 | 8 | 2854.2 | 161 | – |
| 2434 | 13-SEP-07 05.49.30.647893 AM | direct | 149 | 8 | 268651.8 | 161 | – |
| 2436 | 02-SEP-07 06.18.04.378034 AM | direct | 116 | 8 | 6394.8 | 161 | – |
| 2446 | 27-JUL-07 07.03.08.302945 AM | direct | 117 | 8 | 103679.3 | 161 | – |
| 2447 | 27-JUL-08 08.59.10.223344 AM | direct | 101 | 8 | 33893.6 | 161 | – |
| 2432 | 14-SEP-07 09.53.40.223345 AM | direct | 102 | 10 | 10523 | 163 | – |
| 2433 | 13-SEP-07 10.19.00.654279 AM | direct | 103 | 10 | 78 | 163 | – |
| 2355 | 26-JAN-06 09.22.51.962632 AM | online | 104 | 8 | 94513.5 | – | – |
| 2356 | 26-JAN-08 09.22.41.934562 AM | online | 105 | 5 | 29473.8 | – | – |
| 2359 | 08-JAN-06 09.34.13.112233 PM | online | 106 | 9 | 5543.1 | – | – |
| 2360 | 14-NOV-07 12.22.31.223344 PM | online | 107 | 4 | 990.4 | – | – |
| 2361 | 13-NOV-07 01.34.21.986210 PM | online | 108 | 8 | 120131.3 | – | – |
| 2362 | 13-NOV-07 02.41.10.619477 PM | online | 109 | 4 | 92829.4 | – | – |
| 2363 | 23-OCT-07 05.49.56.346122 PM | online | 144 | 0 | 10082.3 | – | – |
| 2364 | 28-AUG-07 06.18.45.942399 PM | online | 145 | 4 | 9500 | – | – |
| 2365 | 28-AUG-07 07.03.34.003399 PM | online | 146 | 9 | 27455.3 | – | – |
| 2366 | 28-AUG-07 08.59.23.144778 PM | online | 147 | 5 | 37319.4 | – | – |
| 2367 | 27-JUN-08 09.53.32.335522 PM | online | 148 | 10 | 144054.8 | – | – |
| 2368 | 26-JUN-08 10.19.43.190089 PM | online | 149 | 10 | 60065 | – | – |
| 2369 | 26-JUN-07 11.22.54.009932 PM | online | 116 | 0 | 11097.4 | – | – |
| 2370 | 27-JUN-08 12.22.11.647398 AM | online | 117 | 4 | 126 | – | – |
| 2371 | 16-MAY-07 01.34.56.113356 AM | online | 118 | 6 | 79405.6 | – | – |
| 2372 | 27-FEB-07 12.22.33.356789 AM | online | 119 | 9 | 16447.2 | – | – |
| 2373 | 27-FEB-08 01.34.51.220065 AM | online | 120 | 4 | 416 | – | – |
| 2374 | 27-FEB-08 02.41.45.109654 AM | online | 121 | 0 | 4797 | – | – |
| 2375 | 26-FEB-07 03.49.50.459233 AM | online | 122 | 2 | 103834.4 | – | – |
| 2376 | 07-JUN-07 06.18.08.883310 AM | online | 123 | 6 | 11006.2 | – | – |
| 2377 | 07-JUN-07 07.03.01.001100 AM | online | 141 | 5 | 38017.8 | – | – |
| 2378 | 24-MAY-07 08.59.10.010101 AM | online | 142 | 5 | 25691.3 | – | – |
| 2380 | 16-MAY-07 09.53.02.909090 AM | online | 143 | 3 | 27132.6 | – | – |
| 2382 | 14-MAY-08 10.19.03.828321 AM | online | 144 | 8 | 71173 | – | – |
| 2383 | 12-MAY-08 11.22.30.545103 AM | online | 145 | 8 | 36374.7 | – | – |
| 2384 | 12-MAY-08 12.22.34.525972 PM | online | 146 | 3 | 29249.1 | – | – |
| 2385 | 08-DEC-07 11.34.11.331392 AM | online | 147 | 4 | 295892 | – | – |
| 2386 | 06-DEC-07 12.22.34.225609 PM | online | 148 | 10 | 21116.9 | – | – |
| 2387 | 11-MAR-07 03.34.56.536966 PM | online | 149 | 5 | 52758.9 | – | – |
| 2388 | 04-JUN-07 04.41.12.554435 PM | online | 150 | 4 | 282694.3 | – | – |
| 2389 | 04-JUN-08 05.49.43.546954 PM | online | 151 | 4 | 17620 | – | – |
| 2390 | 18-NOV-07 04.18.50.546851 PM | online | 152 | 9 | 7616.8 | – | – |
| 2391 | 27-FEB-06 05.03.03.828330 PM | direct | 153 | 2 | 48070.6 | 156 | – |
| 2392 | 21-JUL-07 08.59.57.571057 PM | direct | 154 | 9 | 26632 | 161 | – |
| 2393 | 10-FEB-08 07.53.19.528202 PM | direct | 155 | 4 | 23431.9 | 161 | – |
| 2395 | 02-FEB-06 08.19.11.227550 PM | direct | 156 | 3 | 68501 | 163 | – |
| 2398 | 19-NOV-07 09.22.53.224175 PM | direct | 157 | 9 | 7110.3 | 163 | – |
| 2399 | 19-NOV-07 10.22.38.340990 PM | direct | 158 | 0 | 25270.3 | 161 | – |
| 2400 | 10-JUL-07 01.34.29.559387 AM | direct | 159 | 2 | 69286.4 | 161 | – |
| 2401 | 10-JUL-07 02.22.53.554822 AM | direct | 160 | 3 | 969.2 | 163 | – |
| 2402 | 02-JUL-07 03.34.44.665170 AM | direct | 161 | 8 | 600 | 154 | – |
| 2403 | 01-JUL-07 04.49.13.615512 PM | direct | 162 | 0 | 220 | 154 | – |
| 2404 | 01-JUL-07 04.49.13.664085 PM | direct | 163 | 6 | 510 | 158 | – |
| 2405 | 01-JUL-07 04.49.13.678123 PM | direct | 164 | 5 | 1233 | 159 | – |
| 2407 | 29-JUN-07 07.03.21.526005 AM | direct | 165 | 9 | 2519 | 155 | – |
| 2408 | 29-JUN-07 08.59.31.333617 AM | direct | 166 | 1 | 309 | 158 | – |
| 2409 | 29-JUN-07 09.53.41.984501 AM | direct | 167 | 2 | 48 | 154 | – |
| 2410 | 24-MAY-08 10.19.51.985501 AM | direct | 168 | 6 | 45175 | 156 | – |
| 2411 | 24-MAY-07 11.22.10.548639 AM | direct | 169 | 8 | 15760.5 | 156 | – |
| 2412 | 29-MAR-06 10.22.09.509801 AM | direct | 170 | 9 | 66816 | 158 | – |
| 2413 | 29-MAR-08 01.34.04.525934 PM | direct | 101 | 5 | 48552 | 161 | – |
| 2414 | 29-MAR-07 02.22.40.536996 PM | direct | 102 | 8 | 10794.6 | 153 | – |
| 2415 | 29-MAR-06 01.34.50.545196 PM | direct | 103 | 6 | 310 | 161 | – |
| 2416 | 29-MAR-07 04.41.20.945676 PM | direct | 104 | 6 | 384 | 160 | – |
| 2417 | 20-MAR-07 05.49.10.974352 PM | direct | 105 | 5 | 1926.6 | 163 | – |
| 2418 | 20-MAR-04 04.18.21.862632 PM | direct | 106 | 4 | 5546.6 | 163 | – |
| 2419 | 20-MAR-07 07.03.32.764632 PM | direct | 107 | 3 | 31574 | 160 | – |
| 2420 | 13-MAR-07 08.59.43.666320 PM | direct | 108 | 2 | 29750 | 160 | – |
| 2421 | 12-MAR-07 09.53.54.562432 PM | direct | 109 | 1 | 72836 | – | – |
| 2422 | 16-DEC-07 08.19.55.462332 PM | direct | 144 | 2 | 11188.5 | 153 | – |
| 2423 | 21-NOV-07 10.22.33.362632 AM | direct | 145 | 3 | 10367.7 | 160 | – |
| 2424 | 21-NOV-07 10.22.33.263332 AM | direct | 146 | 4 | 13824 | 153 | – |
| 2425 | 16-NOV-06 11.34.22.162552 PM | direct | 147 | 5 | 1500.8 | 163 | – |
| 2426 | 17-NOV-06 12.22.11.262552 AM | direct | 148 | 6 | 7200 | – | – |
| 2427 | 10-NOV-07 01.34.22.362124 AM | direct | 149 | 7 | 9055 | 163 | – |
| 2428 | 10-NOV-07 02.41.34.463567 AM | direct | 116 | 8 | 14685.8 | – | – |
| 2429 | 10-NOV-07 03.49.25.526321 AM | direct | 117 | 9 | 50125 | 154 | – |
| 2430 | 02-OCT-07 06.18.36.663332 AM | direct | 101 | 8 | 29669.9 | 159 | – |
| 2431 | 14-SEP-06 07.03.04.763452 AM | direct | 102 | 1 | 5610.6 | 163 | – |
| 2437 | 01-SEP-06 08.59.15.826132 AM | direct | 103 | 4 | 13550 | 163 | – |
| 2438 | 01-SEP-07 09.53.26.934626 AM | direct | 104 | 0 | 5451 | 154 | – |
| 2439 | 31-AUG-07 10.19.37.811132 AM | direct | 105 | 1 | 22150.1 | 159 | – |
| 2441 | 01-AUG-08 11.22.48.734526 AM | direct | 106 | 5 | 2075.2 | 160 | – |
| 2442 | 27-JUL-06 12.22.59.662632 PM | direct | 107 | 9 | 52471.9 | 154 | – |
| 2443 | 27-JUL-06 01.34.16.562632 PM | direct | 108 | 0 | 3646 | 154 | – |
| 2444 | 27-JUL-07 02.22.27.462632 PM | direct | 109 | 1 | 77727.2 | 155 | – |
| 2445 | 27-JUL-06 03.34.38.362632 PM | direct | 144 | 8 | 5537.8 | 158 | – |
| 2448 | 18-JUN-07 04.41.49.262632 PM | direct | 145 | 5 | 1388 | 158 | – |
| 2449 | 13-JUN-07 05.49.07.162632 PM | direct | 146 | 6 | 86 | 155 | – |
| 2450 | 11-APR-07 06.18.10.362632 PM | direct | 147 | 3 | 1636 | 159 | – |
| 2451 | 17-DEC-07 05.03.52.562632 PM | direct | 148 | 7 | 10474.6 | 154 | – |
| 2452 | 06-OCT-07 08.59.43.462632 PM | direct | 149 | 5 | 12589 | 159 | – |
| 2453 | 04-OCT-07 09.53.34.362632 PM | direct | 116 | 0 | 129 | 153 | – |
| 2456 | 07-NOV-06 07.53.25.989889 PM | direct | 117 | 0 | 3878.4 | 163 | – |
| 2457 | 31-OCT-07 11.22.16.162632 PM | direct | 118 | 5 | 21586.2 | 159 | – |
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_id | most_recent_order | days_since_last_order |
|---|---|---|
| 107 | 2007-11-14 12.22.31.223344 | 5686 |
| 108 | 2007-11-13 13.34.21.986210 | 5687 |
| 158 | 2007-11-19 22.22.38.340990 | 5680 |
| 161 | 2007-07-02 03.34.44.665170 | 5821 |
| 166 | 2007-06-29 08.59.31.333617 | 5824 |
| 105 | 2008-01-26 09.22.41.934562 | 5613 |
| 109 | 2008-02-10 21.22.35.564789 | 5597 |
| 143 | 2007-05-16 09.53.02.909090 | 5868 |
| 159 | 2007-07-10 01.34.29.559387 | 5813 |
| 162 | 2007-07-01 16.49.13.615512 | 5822 |
| 163 | 2007-07-01 16.49.13.664085 | 5822 |
| 168 | 2008-05-24 10.19.51.985501 | 5494 |
| 104 | 2008-07-14 18.18.23.234567 | 5443 |
| 118 | 2007-10-31 23.22.16.162632 | 5699 |
| 145 | 2008-05-12 11.22.30.545103 | 5506 |
| 121 | 2008-02-27 02.41.45.109654 | 5581 |
| 141 | 2007-06-07 07.03.01.001100 | 5846 |
| 155 | 2008-02-10 19.53.19.528202 | 5597 |
| 101 | 2008-07-27 08.59.10.223344 | 5430 |
| 103 | 2007-10-02 17.49.34.678340 | 5729 |
| 116 | 2007-11-10 02.41.34.463567 | 5690 |
| 120 | 2008-02-27 01.34.51.220065 | 5581 |
| 142 | 2007-05-24 08.59.10.010101 | 5860 |
| 151 | 2008-06-04 17.49.43.546954 | 5483 |
| 156 | 2006-02-02 20.19.11.227550 | 6335 |
| 157 | 2007-11-19 21.22.53.224175 | 5680 |
| 169 | 2007-05-24 11.22.10.548639 | 5860 |
| 146 | 2008-05-12 12.22.34.525972 | 5506 |
| 148 | 2008-06-27 21.53.32.335522 | 5459 |
| 149 | 2008-06-26 22.19.43.190089 | 5460 |
| 122 | 2007-02-26 03.49.50.459233 | 5947 |
| 152 | 2007-11-18 16.18.50.546851 | 5682 |
| 170 | 2006-03-29 10.22.09.509801 | 6281 |
| 144 | 2008-05-14 10.19.03.828321 | 5504 |
| 119 | 2007-02-27 00.22.33.356789 | 5946 |
| 153 | 2006-02-27 17.03.03.828330 | 6311 |
| 164 | 2007-07-01 16.49.13.678123 | 5822 |
| 167 | 2007-06-29 09.53.41.984501 | 5824 |
| 102 | 2007-11-19 14.41.54.696211 | 5681 |
| 106 | 2008-08-01 11.22.48.734526 | 5425 |
| 147 | 2007-12-08 11.34.11.331392 | 5662 |
| 117 | 2008-06-27 00.22.11.647398 | 5460 |
| 123 | 2007-06-07 06.18.08.883310 | 5846 |
| 150 | 2007-06-04 16.41.12.554435 | 5849 |
| 154 | 2007-07-21 20.59.57.571057 | 5801 |
| 160 | 2007-07-10 02.22.53.554822 | 5813 |
| 165 | 2007-06-29 07.03.21.526005 | 5824 |
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_id | manager_id | employee_hire_date | manager_hire_date | months_elapsed |
|---|---|---|---|---|
| 101 | 100 | 2005-09-21 | 2003-06-17 | -27.13 |
| 102 | 100 | 2001-01-13 | 2003-06-17 | 29.13 |
| 114 | 100 | 2002-12-07 | 2003-06-17 | 6.32 |
| 120 | 100 | 2004-07-18 | 2003-06-17 | -13.03 |
| 121 | 100 | 2005-04-10 | 2003-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_id | order_date | ship_by_date | ship_by_datetime | time_remaining |
|---|---|---|---|---|
| 2458 | 2007-08-16 15.34.12.234359 | 2007-08-31 | 2007-08-31 00.00.00.000000 | +000000015 08:25:46.765641 |
| 2397 | 2007-11-19 14.41.54.696211 | 2007-11-30 | 2007-11-30 00.00.00.000000 | +000000011 09:18:04.303789 |
| 2454 | 2007-10-02 17.49.34.678340 | 2007-10-31 | 2007-10-31 00.00.00.000000 | +000000029 06:10:24.321660 |
| 2354 | 2008-07-14 18.18.23.234567 | 2008-07-31 | 2008-07-31 00.00.00.000000 | +000000017 05:41:35.765433 |
| 2358 | 2008-01-08 17.03.12.654278 | 2008-01-31 | 2008-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_id | order_date | ship_by_date | ship_by_datetime | time_remaining | time_remaining_string |
|---|---|---|---|---|---|
| 2458 | 2007-08-16 15.34.12.234359 | 2007-08-31 | 2007-08-31 00.00.00.000000 | +000000015 08:25:46.765641 | 15 days, 8 hours, 25 minutes, 47 seconds |
| 2397 | 2007-11-19 14.41.54.696211 | 2007-11-30 | 2007-11-30 00.00.00.000000 | +000000011 09:18:04.303789 | 11 days, 9 hours, 18 minutes, 4 seconds |
| 2454 | 2007-10-02 17.49.34.678340 | 2007-10-31 | 2007-10-31 00.00.00.000000 | +000000029 06:10:24.321660 | 29 days, 6 hours, 10 minutes, 24 seconds |
| 2354 | 2008-07-14 18.18.23.234567 | 2008-07-31 | 2008-07-31 00.00.00.000000 | +000000017 05:41:35.765433 | 17 days, 5 hours, 41 minutes, 36 seconds |
| 2358 | 2008-01-08 17.03.12.654278 | 2008-01-31 | 2008-01-31 00.00.00.000000 | +000000023 06:56:46.345722 | 23 days, 6 hours, 56 minutes, 46 seconds |