May 23, 2013
The following question recently came through an ERP mailing list (significantly rephrased):
I would like to use the Microsoft Query tool in Microsoft Excel to extract records from the ERP database. I would like the list of parent records to be retrieved into Excel when all of the specified child records belong to the parent.
That sounds as though it would be an easy request. The OP in the ERP mailing list later followed up with the following:
“If I may ask for another variation to pick your brain….
How would I rewrite this to show ONLY the work order(s) that have all of the 3 Parts, but instead of showing all of them, show only the ones that don’t include a specific other part?
For example, Include all the work order retrieved from your query, but then subtract those work orders that don’t contain a part or parts in another separate list.
I.E. All order containing parts (‘147555′,’227643′,’155328’) but not containing ANY of these parts (‘12345’, ‘456789’)”
In the above follow up, the term “work order(s)” refer to the “parent records” that was previously mentioned, and the term “parts” refer to the “child records” that was previously mentioned. Below is a graphical mock up, with the light-green colored rectangles representing the “parts”, and the white colored rectangle at the bottom left representing the “work order”. The other white colored rectangles represent subassemblies that belong to the “work order”, and the cyan colored rectangles represent the operations that utilize the “parts”. Through normalization, the white colored rectangles represent rows in one database table (WORK_ORDER – the “parent” table), the cyan colored rectangles represent rows in another table (OPERATION – the “child” table), and the light-green colored rectangles represent rows in a third table (REQUIREMENT – the “grandchild” table).
The first request still seems to be easy to solve, even though there are three tables that are potentially involved. Let’s simplify the problem further and just focus on the “grandchild” table. Let’s create a small version of that table and populate the table with some sample rows:
CREATE TABLE T1 ( WORKORDER_TYPE VARCHAR2(1), WORKORDER_BASE_ID VARCHAR2(15), WORKORDER_LOT_ID VARCHAR2(3), WORKORDER_SUB_ID VARCHAR2(3), PIECE_NO NUMBER, PART_ID VARCHAR2(15), QTY NUMBER(12,4)); INSERT INTO T1 SELECT 'W' WORKORDER_TYPE, TRIM(TO_CHAR(TRUNC(ROWNUM / 100 + 1)*1000,'00000')) || 'W' WORKORDER_BASE_ID, TO_CHAR(MOD(TRUNC(ROWNUM / 25 + 1), 999) + 1) WORKORDER_LOT_ID, TO_CHAR(TRUNC(MOD(ROWNUM - 1, 25) / 5) + 1) WORKORDER_SUB_ID, MOD(ROWNUM - 1, 25) + 1 PIECE_NO, CHR(65 + MOD(ROWNUM - 1, 26)) || CHR(65 + MOD(ROWNUM - 1, 15)) || MOD(ROWNUM - 1, 10) PART_ID, TRUNC(DBMS_RANDOM.VALUE*100) + 1 QTY FROM DUAL CONNECT BY LEVEL<=1000;
In the above, the combination of the WORKORDER_BASE_ID column and the WORKORDER_LOT_ID column represent the “work order” that was mentioned by the OP. The WORKORDER_SUB_ID column identifies the subassembly number (answering the question of to which of the white colored rectangles does the “part” belong). The PIECE_NO column is used to uniquely identify the rows that belong to a specific subassembly number (that is the final column in the REQUIREMENT table’s primary key). The PART_ID column is the specific “part” that is attached to a specific “work order”. The QTY column indicates the number of the “part” that is required by the “work order” – with the sample data, this is the only column that will differ from one execution of the table creation script to the next execution.
The sample data is a bit too consistent, however, we will ignore any issues that consistency may present for now. Let’s execute a simple SQL statement that indicates how many from the following “parts” list appear in the “work orders”: MI8, NJ9, KG6 (note the use of DISTINCT – a single PART_ID could appear on more than one subassembly in a single WORKORDER_BASE_ID, WORKORDER_LOT_ID combination, and we do not want to repeatedly count those “parts”):
SELECT T.WORKORDER_TYPE, T.WORKORDER_BASE_ID, T.WORKORDER_LOT_ID, T.WORKORDER_SUB_ID, T.PART_ID, COUNT(DISTINCT T.PART_ID) OVER (PARTITION BY T.WORKORDER_TYPE, T.WORKORDER_BASE_ID, T.WORKORDER_LOT_ID) AS C FROM T1 TWHERE T.PART_ID IN ('MI8','NJ9','KG6'); W WORKORDER_BASE_ WOR WOR PART_ID C - --------------- --- --- --------------- ---------- W 01000W 3 3 KG6 3 W 01000W 3 3 MI8 3 W 01000W 3 3 NJ9 3 W 05000W 19 1 KG6 3 W 05000W 19 1 MI8 3 W 05000W 19 1 NJ9 3 W 09000W 34 4 KG6 3 W 09000W 34 4 MI8 3 W 09000W 34 4 NJ9 3
Only 9 rows returned, with three different WORKORDER_BASE_ID, WORKORDER_LOT_ID combinations. For a moment, let’s ignore that the three “parts” appear in all three “work orders”. If we wanted a list of those “work orders” where _all_ three of the “parts” are present, and not just _one or more_ of the three “parts”, we could simply slide the above SQL statement into an inline view and specify that our C generated column must be equal to (or greater than – the use of greater than will be explained later) 3:
SELECT WORKORDER_TYPE, WORKORDER_BASE_ID, WORKORDER_LOT_ID, WORKORDER_SUB_ID, PART_ID FROM (SELECT T.WORKORDER_TYPE, T.WORKORDER_BASE_ID, T.WORKORDER_LOT_ID, T.WORKORDER_SUB_ID, T.PART_ID, COUNT(DISTINCT T.PART_ID) OVER (PARTITION BY T.WORKORDER_TYPE, T.WORKORDER_BASE_ID, T.WORKORDER_LOT_ID) AS C FROM T1 T WHERE T.PART_ID IN ('MI8','NJ9','KG6')) WHERE C>=3 ORDER BY WORKORDER_BASE_ID, WORKORDER_LOT_ID, WORKORDER_SUB_ID; W WORKORDER_BASE_ WOR WOR PART_ID - --------------- --- --- ------- W 01000W 3 3 KG6 W 01000W 3 3 MI8 W 01000W 3 3 NJ9 W 05000W 19 1 KG6 W 05000W 19 1 MI8 W 05000W 19 1 NJ9 W 09000W 34 4 KG6 W 09000W 34 4 MI8 W 09000W 34 4 NJ9
While trying to determine the OP’s business case for the requests, I wondered if the OP was attempting to replace a specific “part” with another “part” within the ERP package, and simply required a report to understand the progress of that manual “part” replacement procedure in the ERP package. Let’s move forward with this assumption by replacing a row containing a specific PART_ID value with a new PART_ID value, but only for a single “work order”:
DELETE FROM T1 WHERE WORKORDER_TYPE='W' AND WORKORDER_BASE_ID='09000W' AND WORKORDER_LOT_ID='34' AND PART_ID='NJ9'; 1 row deleted. INSERT INTO T1 VALUES ( 'W', '09000W', '34', '4', 10, 'REPLACEMENT-NJ9', 10); 1 row created.
Let’s check the result of the SQL statement that we were in the process of developing:
SELECT WORKORDER_TYPE, WORKORDER_BASE_ID, WORKORDER_LOT_ID, WORKORDER_SUB_ID, PART_ID FROM (SELECT T.WORKORDER_TYPE, T.WORKORDER_BASE_ID, T.WORKORDER_LOT_ID, T.WORKORDER_SUB_ID, T.PART_ID, COUNT(DISTINCT T.PART_ID) OVER (PARTITION BY T.WORKORDER_TYPE, T.WORKORDER_BASE_ID, T.WORKORDER_LOT_ID) AS C FROM T1 T WHERE T.PART_ID IN ('MI8','NJ9','KG6')) WHERE C>=3 ORDER BY WORKORDER_BASE_ID, WORKORDER_LOT_ID, WORKORDER_SUB_ID; W WORKORDER_BASE_ WOR WOR PART_ID - --------------- --- --- ------- W 01000W 3 3 KG6 W 01000W 3 3 MI8 W 01000W 3 3 NJ9 W 05000W 19 1 KG6 W 05000W 19 1 MI8 W 05000W 19 1 NJ9
Now, there are only six rows returned from two “work orders” – the third “work order” is no longer included in the query results.
With the use of the C >= n syntax, we are able to find “work orders” with at least n number of the specified “parts”. So, if we were interested in finding cases where at least two of the three listed “parts” are attached to a “work order”, we just need to change the 3 to 2:
SELECT WORKORDER_TYPE, WORKORDER_BASE_ID, WORKORDER_LOT_ID, WORKORDER_SUB_ID, PART_ID FROM (SELECT T.WORKORDER_TYPE, T.WORKORDER_BASE_ID, T.WORKORDER_LOT_ID, T.WORKORDER_SUB_ID, T.PART_ID, COUNT(DISTINCT T.PART_ID) OVER (PARTITION BY T.WORKORDER_TYPE, T.WORKORDER_BASE_ID, T.WORKORDER_LOT_ID) AS C FROM T1 T WHERE T.PART_ID IN ('MI8','NJ9','KG6')) WHERE C>=2 ORDER BY WORKORDER_BASE_ID, WORKORDER_LOT_ID, WORKORDER_SUB_ID; W WORKORDER_BASE_ WOR WOR PART_ID - --------------- --- --- ------- W 01000W 3 3 KG6 W 01000W 3 3 MI8 W 01000W 3 3 NJ9 W 05000W 19 1 KG6 W 05000W 19 1 NJ9 W 05000W 19 1 MI8 W 09000W 34 4 KG6 W 09000W 34 4 MI8
The third “work order” is again included in the query resultset.
Other than putting the results into Microsoft Excel, the sample query satisfies the first request of the OP. If we were not interested in returning the WORKORDER_SUB_ID and PART_ID columns, we could have simply used the following SQL statement:
SELECT T.WORKORDER_TYPE, T.WORKORDER_BASE_ID, T.WORKORDER_LOT_ID FROM T1 T WHERE T.PART_ID IN ('MI8','NJ9','KG6') GROUP BY T.WORKORDER_TYPE, T.WORKORDER_BASE_ID, T.WORKORDER_LOT_ID HAVING COUNT(*)>=3; W WORKORDER_BASE_ WOR - --------------- --- W 05000W 19 W 01000W 3
—
Now to work on the second request, only retrieving the “work orders” when all of a specified list of “parts” are attached to the “work order” and none of another list of “parts” are attached to the “work order”. As a starting point, we will re-use the inline view from the previous solution. The IN list will be modified to include the “parts” that must be attached to the “work order”, as well as the “parts” that must not be attached to the “work order”. The COUNT analytic function must be modified to include either a DECODE or CASE structure to divide the “parts” into the “must” and “must not” lists so that a count of each may be obtained:
SELECT T.WORKORDER_TYPE, T.WORKORDER_BASE_ID, T.WORKORDER_LOT_ID, T.WORKORDER_SUB_ID, T.PART_ID, COUNT(DISTINCT (CASE WHEN T.PART_ID IN ('MI8','NJ9','KG6') THEN T.PART_ID ELSE NULL END)) OVER (PARTITION BY T.WORKORDER_TYPE, T.WORKORDER_BASE_ID, T.WORKORDER_LOT_ID) AS C, COUNT(DISTINCT (CASE WHEN T.PART_ID IN ('REPLACEMENT-NJ9') THEN T.PART_ID ELSE NULL END)) OVER (PARTITION BY T.WORKORDER_TYPE, T.WORKORDER_BASE_ID, T.WORKORDER_LOT_ID) AS NC FROM T1 T WHERE T.PART_ID IN ('MI8','KG6', 'REPLACEMENT-NJ9'); W WORKORDER_BASE_ WOR WOR PART_ID C NC - --------------- --- --- --------------- ---------- ---------- W 01000W 3 3 KG6 2 0 W 01000W 3 3 MI8 2 0 W 05000W 19 1 KG6 2 0 W 05000W 19 1 MI8 2 0 W 09000W 34 4 KG6 2 1 W 09000W 34 4 MI8 2 1 W 09000W 34 4 REPLACEMENT-NJ9 2 1
To satisfy the OP’s second request, we are interested in those rows where the C column value is (at least) 2 and the NC column value is 0. Just as before, we will slide the above SQL statement into an inline view and add the specifications for the C and NC generated columns:
SELECT WORKORDER_TYPE, WORKORDER_BASE_ID, WORKORDER_LOT_ID, WORKORDER_SUB_ID, PART_ID FROM (SELECT T.WORKORDER_TYPE, T.WORKORDER_BASE_ID, T.WORKORDER_LOT_ID, T.WORKORDER_SUB_ID, T.PART_ID, COUNT(DISTINCT (CASE WHEN T.PART_ID IN ('MI8','NJ9','KG6') THEN T.PART_ID ELSE NULL END)) OVER (PARTITION BY T.WORKORDER_TYPE, T.WORKORDER_BASE_ID, T.WORKORDER_LOT_ID) AS C, COUNT(DISTINCT (CASE WHEN T.PART_ID IN ('REPLACEMENT-NJ9') THEN T.PART_ID ELSE NULL END)) OVER (PARTITION BY T.WORKORDER_TYPE, T.WORKORDER_BASE_ID, T.WORKORDER_LOT_ID) AS NC FROM T1 T WHERE T.PART_ID IN ('MI8','KG6', 'REPLACEMENT-NJ9')) WHERE C>=2 AND NC=0 ORDER BY WORKORDER_BASE_ID, WORKORDER_LOT_ID, WORKORDER_SUB_ID; W WORKORDER_BASE_ WOR WOR PART_ID - --------------- --- --- ------- W 01000W 3 3 KG6 W 01000W 3 3 MI8 W 05000W 19 1 KG6 W 05000W 19 1 MI8
The above shows those “work orders” where the NJ9 “part” had not yet been replaced with the REPLACEMENT-NJ9 “part”. If desired, we are also able to easily modify the OP’s second request to see the “work orders” where the “part” was already replaced just by changing NC=0 to NC=1:
SELECT WORKORDER_TYPE, WORKORDER_BASE_ID, WORKORDER_LOT_ID, WORKORDER_SUB_ID, PART_ID FROM (SELECT T.WORKORDER_TYPE, T.WORKORDER_BASE_ID, T.WORKORDER_LOT_ID, T.WORKORDER_SUB_ID, T.PART_ID, COUNT(DISTINCT (CASE WHEN T.PART_ID IN ('MI8','NJ9','KG6') THEN T.PART_ID ELSE NULL END)) OVER (PARTITION BY T.WORKORDER_TYPE, T.WORKORDER_BASE_ID, T.WORKORDER_LOT_ID) AS C, COUNT(DISTINCT (CASE WHEN T.PART_ID IN ('REPLACEMENT-NJ9') THEN T.PART_ID ELSE NULL END)) OVER (PARTITION BY T.WORKORDER_TYPE, T.WORKORDER_BASE_ID, T.WORKORDER_LOT_ID) AS NC FROM T1 T WHERE T.PART_ID IN ('MI8','KG6', 'REPLACEMENT-NJ9')) WHERE C>=2 AND NC=1 ORDER BY WORKORDER_BASE_ID, WORKORDER_LOT_ID, WORKORDER_SUB_ID; W WORKORDER_BASE_ WOR WOR PART_ID - --------------- --- --- --------------- W 09000W 34 4 KG6 W 09000W 34 4 REPLACEMENT-NJ9 W 09000W 34 4 MI8
What other related information are we able to return? What about all “parts” (the full bill of material) for each of the work orders that match the OP’s second request. That modification is simple, just slide the majority of the above SQL statement into a subquery (found in the first WHERE clause):
SELECT * FROM T1 WHERE (WORKORDER_TYPE, WORKORDER_BASE_ID, WORKORDER_LOT_ID) IN (SELECT WORKORDER_TYPE, WORKORDER_BASE_ID, WORKORDER_LOT_ID FROM (SELECT T.WORKORDER_TYPE, T.WORKORDER_BASE_ID, T.WORKORDER_LOT_ID, T.WORKORDER_SUB_ID, T.PART_ID, COUNT(DISTINCT (CASE WHEN T.PART_ID IN ('MI8','NJ9','KG6') THEN T.PART_ID ELSE NULL END)) OVER (PARTITION BY T.WORKORDER_TYPE, T.WORKORDER_BASE_ID, T.WORKORDER_LOT_ID) AS C, COUNT(DISTINCT (CASE WHEN T.PART_ID IN ('REPLACEMENT-NJ9') THEN T.PART_ID ELSE NULL END)) OVER (PARTITION BY T.WORKORDER_TYPE, T.WORKORDER_BASE_ID, T.WORKORDER_LOT_ID) AS NC FROM T1 T WHERE T.PART_ID IN ('MI8','KG6', 'REPLACEMENT-NJ9')) WHERE C>=2 AND NC=0) ORDER BY WORKORDER_BASE_ID, WORKORDER_LOT_ID, WORKORDER_SUB_ID, PIECE_NO; W WORKORDER_BASE_ WOR WOR PIECE_NO PART_ID QTY - --------------- --- --- ---------- --------------- ---------- W 01000W 3 1 1 ZK5 48 W 01000W 3 1 2 AL6 60 W 01000W 3 1 3 BM7 42 W 01000W 3 1 4 CN8 26 W 01000W 3 1 5 DO9 49 W 01000W 3 2 6 EA0 66 W 01000W 3 2 7 FB1 39 W 01000W 3 2 8 GC2 57 W 01000W 3 2 9 HD3 42 W 01000W 3 2 10 IE4 52 W 01000W 3 3 11 JF5 16 W 01000W 3 3 12 KG6 61 W 01000W 3 3 13 LH7 39 W 01000W 3 3 14 MI8 75 W 01000W 3 3 15 NJ9 94 W 01000W 3 4 16 OK0 67 W 01000W 3 4 17 PL1 12 W 01000W 3 4 18 QM2 41 W 01000W 3 4 19 RN3 97 W 01000W 3 4 20 SO4 15 W 01000W 3 5 21 TA5 54 W 01000W 3 5 22 UB6 59 W 01000W 3 5 23 VC7 23 W 01000W 3 5 24 WD8 7 W 01000W 3 5 25 YJ4 22 W 05000W 19 1 1 JF5 31 W 05000W 19 1 2 KG6 42 W 05000W 19 1 3 LH7 37 W 05000W 19 1 4 MI8 85 W 05000W 19 1 5 NJ9 100 W 05000W 19 2 6 OK0 43 W 05000W 19 2 7 PL1 58 W 05000W 19 2 8 QM2 76 W 05000W 19 2 9 RN3 66 W 05000W 19 2 10 SO4 75 W 05000W 19 3 11 TA5 50 W 05000W 19 3 12 UB6 55 W 05000W 19 3 13 VC7 18 W 05000W 19 3 14 WD8 33 W 05000W 19 3 15 XE9 8 W 05000W 19 4 16 YF0 86 W 05000W 19 4 17 ZG1 7 W 05000W 19 4 18 AH2 25 W 05000W 19 4 19 BI3 38 W 05000W 19 4 20 CJ4 34 W 05000W 19 5 21 DK5 88 W 05000W 19 5 22 EL6 91 W 05000W 19 5 23 FM7 52 W 05000W 19 5 24 GN8 71 W 05000W 19 5 25 IE4 55
Just as before, we are able to see all “parts” for the “work orders” that already have the replacement part, just by changing the NC=0 to NC=1:
SELECT * FROM T1 WHERE (WORKORDER_TYPE, WORKORDER_BASE_ID, WORKORDER_LOT_ID) IN (SELECT WORKORDER_TYPE, WORKORDER_BASE_ID, WORKORDER_LOT_ID FROM (SELECT T.WORKORDER_TYPE, T.WORKORDER_BASE_ID, T.WORKORDER_LOT_ID, T.WORKORDER_SUB_ID, T.PART_ID, COUNT(DISTINCT (CASE WHEN T.PART_ID IN ('MI8','NJ9','KG6') THEN T.PART_ID ELSE NULL END)) OVER (PARTITION BY T.WORKORDER_TYPE, T.WORKORDER_BASE_ID, T.WORKORDER_LOT_ID) AS C, COUNT(DISTINCT (CASE WHEN T.PART_ID IN ('REPLACEMENT-NJ9') THEN T.PART_ID ELSE NULL END)) OVER (PARTITION BY T.WORKORDER_TYPE, T.WORKORDER_BASE_ID, T.WORKORDER_LOT_ID) AS NC FROM T1 T WHERE T.PART_ID IN ('MI8','KG6', 'REPLACEMENT-NJ9')) WHERE C>=2 AND NC=1) ORDER BY WORKORDER_BASE_ID, WORKORDER_LOT_ID, WORKORDER_SUB_ID, PIECE_NO; W WORKORDER_BASE_ WOR WOR PIECE_NO PART_ID QTY - --------------- --- --- ---------- --------------- ---------- W 09000W 34 1 1 UF0 80 W 09000W 34 1 2 VG1 75 W 09000W 34 1 3 WH2 75 W 09000W 34 1 4 XI3 77 W 09000W 34 1 5 YJ4 46 W 09000W 34 2 6 ZK5 57 W 09000W 34 2 7 AL6 64 W 09000W 34 2 8 BM7 14 W 09000W 34 2 9 CN8 30 W 09000W 34 2 10 DO9 62 W 09000W 34 3 11 EA0 45 W 09000W 34 3 12 FB1 68 W 09000W 34 3 13 GC2 25 W 09000W 34 3 14 HD3 73 W 09000W 34 3 15 IE4 32 W 09000W 34 4 10 REPLACEMENT-NJ9 10 W 09000W 34 4 16 JF5 49 W 09000W 34 4 17 KG6 23 W 09000W 34 4 18 LH7 60 W 09000W 34 4 19 MI8 75 W 09000W 34 5 21 OK0 91 W 09000W 34 5 22 PL1 66 W 09000W 34 5 23 QM2 45 W 09000W 34 5 24 RN3 49 W 09000W 34 5 25 TE9 6
—
Using the sample table and data provided, are there any other solutions to the OP’s requests?
Recent Comments