DB2 concatenate unique column values into one row, comma seperated - sql
Two tables:
Parts Table:
Part_Number Load_Date TQTY
m-123 19940102 32
1234Cf 20010809 3
wf9-2 20160421 14
Locations Table:
PartNo Condition Location QTY
m-123 U A02 2
1234Cf S A02 3
m-123 U B01 1
wf9-2 S A06 7
m-123 S A18 29
wf9-2 U F16 7
Result:
Part_Number Load_Date TQTY U_LOC UQTY S_LOC SQTY
m-123 19940102 32 A02,B01 3 A18 29
1234Cf 20010809 3 A02 3
wf9-2 20160421 14 F16 7 A06 7
I am having trouble finding a solution to this with my current DB2 version. I am not completely sure how to find the version, but it is running on an AS400 system, and it seems the version of DB2, is tied to the OS version. Which the box is using: Operating system: i5/OS Version: V5R4M0
(I tried some commands to get the DB2 version using these suggestions Here but none of them worked, like most stated).
In regards to concatenating multiple rows of column data into one row I have come across many articles stating to use XMLAGG or xmlserialize, Here and, Here but I get an error stating the command is not recognized.
Not sure where to go from here, as there seem to be solutions, but I can't get those already suggested functions to work.
EDIT:
Using the accepted answer and explanation, as well as the example
HERE to get a basic idea of recursion with a simple example, and it was
HERE using the "SELECT rownumber() over(partition by category)" statements that really helped pull it all together. Once I understood that statement of course.
I also learned to make sure the data used in the recursion is as narrowed down as possible and then joined up with extra data later. This makes for exponentially faster results. <-- This seems pretty obvious, but when trying to figure all of this out, it wasn't obvious and my query was pretty slow. Once I understood what was actually happening better it was easier to make adjustments for really fast results.
This is rather complicated, so I will show all my work:
Table definitions
create table parts
(part_number Varchar(64),
load_date Date,
total_qty Dec(5,0));
create table locations
(part_number Varchar(64),
condition Char(1),
location Char(3),
qty Dec(5,0));
insert into parts
values ('m-123', '1994-01-02', 32),
('1234Cf', '2001-08-09', 3),
('wf9-2', '2016-04-21', 14);
insert into locations
values ('m-123', 'U', 'A02', 2),
('1234Cf', 'S', 'A02', 3),
('m-123', 'U', 'B01', 1),
('wf9-2', 'S', 'A06', 7),
('m-123', 'S', 'A18', 29),
('wf9-2', 'U', 'F16', 7);
The query:
with -- CTE's
-- This collects locations into a comma seperated list
tmp (part_number, condition, location, csv, level) as (
select part_number, condition, min(location),
cast(min(location) as varchar(128)), 1
from locations
group by part_number, condition
union all
select a.part_number, a.condition, b.location,
a.csv || ',' || b.location, a.level + 1
from tmp a
join locations b using (part_number, condition)
where a.csv not like '%' || b.location || '%'
and b.location > a.location),
-- This chooses the correct csv list, and adds quantity for the condition
tmp2 (part_number, condition, csv, qty) as (
select t.part_number, t.condition, t.csv,
(select sum(qty) qty
from locations
where part_number = t.part_number
and condition = t.condition)
from tmp t
where level = (select max(level)
from tmp
where part_number = t.part_number
and condition = t.condition))
-- This is the final select that combines the parts file with
-- the second stage CTE and arranges things horizontally by condition
select p.part_number, p.load_date,
(select sum(qty)
from locations
where part_number = p.part_number) as total_qty,
coalesce(u.csv, '') as u_loc,
coalesce(u.qty, 0) as uqty,
coalesce(s.csv, '') as s_loc,
coalesce(s.qty, 0) as sqty
from parts p
left outer join tmp2 u
on u.part_number = p.part_number and u.condition = 'U'
left outer join tmp2 s
on s.part_number = p.part_number and s.condition = 'S'
order by p.load_date;
EDIT I have had to add some extra bits in here to support more than two locations for a part/condition, and I have made the column naming in the CTEs more consistent. Ok, so let me explain this a bit, there are 3 parts to this quety, 2 CTEs and the query, you can see the three parts are separated by comments. The first CTE is a recursive CTE. It's purpose is to produce the comma separated location list. You should be able to run the select by itself to see just what it does. tmp is the table name, part_number, condition, csv, and level are the column names. A recursive CTE needs a SELECT to prime the CTE and a UNION ALL with a SELECT that fills in the next details. In this case the priming SELECT retrieves a part number, a condition, and the first location (alphabetically) for that combination. level is set to 1. If you run just the priming select, you will get:
part_number condition location csv level
----------- --------- -------- --- -----
1234Cf S A01 A02 1
m-123 S A18 A18 1
m-123 U A02 A02 1
wf9-2 U F16 F16 1
wf9-2 S A06 A06 1
Note one line per part/condition. The remainder of the recursive CTE will fill in the remaining locations in csv, but it will actually add additional records so we need to filter the results here and later. So records are processed as they are added. The first rows listed above are joined with the location file
on part_number and condition. Note in the priming select I have a cast of the second min(location) to a varchar(128). This leaves room for the CSV column to expand. Without this, it will still expand, but not enough to hold more than 2 locations.
The second select in the recursive CTE concatenates a comma and the next location to the end of CSV. The specific bit that does this is a.csv || ',' || b.location. It also increments the level column. This helps us keep track of where we are in the query. Eventually, the row with the highest level is the one we want to use. We also have a way to end the recursion, and some filters to reduce the number of rows added to the temporary result set. If we have 2 locations, A02 and B02, left unchecked, we will get the following rows: A02, A02,A02, A02,B02, A02,A02,A02, A02,B02,A02, A02,A02,B02, A02,B02,B02, ... ad infinitum. The anti-duplication filter where a.csv not like '%' || b.location || '%' is sufficient for two locations to end the recursion, and minimize rows, like above, for locations A02 and B02, with the anti-duplication filter, we will get rows A02, and A02,B02. Note that none of the other results from the first example with duplicate locations are returned. Adding a third location C02 will yield, with anti-duplication filter, the following rows: A02, A02,B02, A02,C02, A02,B02,C02, A02,C02,B02. No duplicates here, but we do have redundant rows, and as you add locations, it gets worse. This is where we need a way to detect these redundant rows. Since we are starting with the lowest location number, we can always make sure that locations added to CSV are greater than the previously added location. To do that all we need to do is include a column in the result set that indicates which column was added (we could interrogate CSV, but that is harder). This is why we need the location column in tmp. Then we can write filter b.location > a.location. In the above 3 location example, this filter prevents row A02,C02,B02 leaving just a single row with all three locations. Adding more than three locations to the locations file will cause the number of rows to expand even more in TMP, but for each part and condition, there will only be one row with all locations, and it will contain all locations in ascending order.
The second CTE does two things. First, it filters TMP to drop all but the rows containing all locations for a given part/condition. Second, it accumulates the total quantity for each part/condition.
The bit that performs the filtering is in the where clause:
where level = (select max(level)
from tmp
where part_number = t.part_number
and condition = t.condition))
Pretty straight forward. The bit that accumulates the total quantity for a part/condition is also an easy to understand sub-query:
(select sum(qty) qty
from locations
where part_number = t.part_number
and condition = t.condition)
The final piece of this monster query is the main select. It joins the parts file with the results of the second CTE to form the ultimate result set:
select p.part_number, p.load_date,
(select sum(qty) from locations where part_number = p.part_number) as total_qty,
coalesce(u.csv, '') as u_loc, coalesce(u.qty, 0) as uqty,
coalesce(s.csv, '') as s_loc, coalesce(s.qty, 0) as sqty
from parts p
left outer join tmp2 u
on u.part_number = p.part_number and u.condition = 'U'
left outer join tmp2 s
on s.part_number = p.part_number and s.condition = 'S'
order by p.load_date
Bits of note are the subquery to retrieve the total quantity from the locations table. You could use the tqty field in parts, but that can get out of sync with the actual quantities in the locations table. In addition there are two left outer joins with tmp2, one for condition U, and another for condition S. These construct the horizontal array of Location/Quantity in the result row. The last thing is the coalesce functions. These give null values (when a result from an outer join is missing) a default value.
End of EDIT
The final result is:
part_number load_date tqty u_loc uqty s_loc sqty
----------- ---------- ---- ------- ---- ----- ----
m-123 1994-01-02 32 A02,B01 3 A18 29
1234Cf 2001-08-09 3 0 A02 3
wf9-2 2016-04-21 14 F16 7 A06 7
Note XMLAGG and XMLSERIALIZE became available at DB2 for i v7.1 and LISTAGG became available at DB2 for i v7.2. Most recent version as of 8/9/2017 is v7.3. As you are on v5r4, it is likely you will need not only a software, but also a hardware upgrade to get current.
No idea what the rules are for UQTY, S_LOC, SQTY but here is the column you asked about ---
SELECT
P.Part_Number,
P.Load_Date,
P.TQTY,
LISTAGG(L.Location, ', ') WITHIN GROUP (ORDER BY L.Location) AS U_LOC
FROM "Parts Table" AS P
LEFT JOIN "Locations Table" AS L ON P.Part_Number = L.Part_Number
GROUP BY P.Part_Number, P.Load_Date, P.TQTY
Related
SQL Server: Two COUNTs in one query multiplying with one another in output
I have a query is used to display information in a queue and part of that information is showing the amount of child entities (packages and labs) that belong to the parent entity (change). However instead of showing the individual counts of each type of child, they multiply with one another. In the below case, there are supposed to be 3 labs and 18 packages, however the the multiply with one another and the output is 54 of each. Below is the offending portion of the query. SELECT cef.ChangeId, COUNT(pac.PackageId) AS 'Packages', COUNT(lab.LabRequestId) AS 'Labs' FROM dbo.ChangeEvaluationForm cef LEFT JOIN dbo.Lab ON cef.ChangeId = Lab.ChangeId LEFT JOIN dbo.Package pac ON (cef.ChangeId = pac.ChangeId AND pac.PackageStatus != 6 AND pac.PackageStatus !=7) WHERE cef.ChangeId = 255 GROUP BY cef.ChangeId I feel like this is obvious but it's not occurring to me how to fix it so the two counts are independent of one another like to me they should be. There doesn't seem to be a scenario like this in any of my research either. Can anyone guide me in the right direction?
Because you do multiply source rows by each left join. So sometimes you have more likely cross join here. SELECT cef.ChangeId, p.Packages, l.Labs FROM dbo.ChangeEvaluationForm cef OUTER APPLY( SELECT COUNT(*) as Labs FROM dbo.Lab WHERE cef.ChangeId = Lab.ChangeId ) l OUTER APPLY( SELECT COUNT(*) AS Packages FROM dbo.Package pac WHERE (cef.ChangeId = pac.ChangeId AND pac.PackageStatus != 6 AND pac.PackageStatus !=7) ) p WHERE cef.ChangeId = 255 GROUP BY cef.ChangeId perhaps GROUP BY is not needed now.
From you question its difficult to derive what result do you expect from your query. So I presume you want following result: +----------+----------+------+ | ChangeId | Packages | Labs | +----------+----------+------+ | 255 | 18 | 3 | +----------+----------+------+ Try below query if you are looking for above mentioned result. SELECT cef.ChangeId, ISNULL(pac.PacCount, 0) AS 'Packages', ISNULL(Lab.LabCount, 0) AS 'Labs' FROM dbo.ChangeEvaluationForm cef LEFT JOIN (SELECT Lab.ChangeId, COUNT(*) LabCount FROM dbo.Lab GROUP BY) Lab ON cef.ChangeId = Lab.ChangeId LEFT JOIN (SELECT pac.ChangeId, COUNT(*) PacCount FROM dbo.Package pac WHERE pac.PackageStatus != 6 AND pac.PackageStatus !=7 GROUP BY pac.ChangeId) pac ON cef.ChangeId = pac.ChangeId WHERE cef.ChangeId = 255 Query Explanation: In your query you didn't use group by, so it ended up giving you 54 as count which is Cartesian product. In this query I tried to group by 'ChangeId' and find aggregate before joining tables. So 3 labs and 18 packages will be counted before join. Your will also notice that I have moved PackageStatus filter before group by in pac table. So unwanted record won't mess with our count.
You start with a particular ChangeId from the dbo.ChangeEvaluationForm table (ChangeId = 255 from your example), then join to the dbo.Lab table. This join makes your result go from 1 row to 3, considering there are 3 Labs with ChangeId = 255. Your problem is on the next join, you are joining all 3 resulting rows from the previous join with the dbo.Package table, which has 18 rows for ChangeId = 255. The resulting count for columns pac.PackageId and lab.LabRequestId will then be 3 x 18 = 54. To get what you want, there are 2 easy solutions: Use COUNT DISTINCT instead of COUNT. This will just count the different values of pac.PackageId and lab.LabRequestId and not the repeated ones. Split the joins into 2 subqueries and join their result (by ChangeId)
Postgres ordering not working as expected
Below is the query i am using : SELECT T.abcd, String_agg(T.yyy, ',') AS yyys, T.bbb FROM (SELECT s.abcd, up.yyy, s.bbb, s.secondary_id FROM A s join B su ON su.search_term_id = s.id join lll_yyy up ON up.lll = su.lll ORDER BY s.abcd su.page_no, su.position) T GROUP BY T.abcd, T.bbb Basically the order of data produced after my aggregated function is not as expected . The output should be sorted by abcd and page_no and position . Expected output: A | 1,2,3,4 | XX Actual Output A |2,4,1,3 | XX the second column in not sorted based on page_no,position as given in the query . The abcd column has a wide variety of data with numbers,special chars etc. example: 0900 dr jne pink, 0900 dr jne pink,098 lakhani shoe, iphone, c??mpu men shoe sport are some sample terms in the abcd column I tried using collate "C" option Is there a way to figure out which word is screwing up the sort order
Use the ORDER BY clause in the aggregate expression: SELECT T.abcd, String_agg(T.yyy, ',' ORDER BY s.abcd, su.page_no, su.position) AS yyys, T.bbb FROM (SELECT s.abcd, up.yyy, s.bbb, s.secondary_id FROM A s join B su ON su.search_term_id = s.id join lll_yyy up ON up.lll = su.lll) T GROUP BY T.abcd, T.bbb The ORDER BY clause in a derived table is ignored.
You are showing an incomplete query. However, it seems to be like this: SELECT ... FROM (SELECT ... ORDER BY ...) Your main select doesn't have an ORDER BY clause here. You can hence get the result rows in any order. The ORDER BY clause in the subquery can be ignored by the DBMS, because data in a table (which includes derived tables, i.e. subqueries) is considered an unordered set.
SQL Combing the top 2 field values into 1 value
I have a very simple query that returns the Notes field. Since there can be multiple notes, I only want the top 2. No problem. However, I'm going to be using the sql within another query. I really don't want 2 lines in my results. I would like to combine the results into 1 field value so I only have 1 result line in the results. Is this possible? For example, I currently get the following: 12345 1001 500.00 "Note 1" 12345 1001 500.00 "Note 2" What I would like to see is this: 12345 1001 500.00 "Note 1 AND Note 2" Following is the sql: select top 2 rcai.field_value from rnt_agrs ra inner join rnt_agr_inv_notes rain on ra.rnt_agr_nbr=rain.rea_rnt_agr_nbr inner join RNT_CUST_ADDNL_INFO rcai on rain.rea_rnt_agr_nbr=rcai.rea_rnt_agr_nbr and rain.bac_acc_id=rcai.bac_acct_id where ra.rnt_agr_nbr=128260511 Thanks for your help. I appreciate this forum for help with these issues.....
Get the next row's value and filter all but the first row: select ..., rcai.field_value || ' AND ' min(rcai.field_value) -- next row's value (same as LEAD in Standard SQL) over (partition by ra.rnt_agr_nbr order by rcai.field_value rows between 1 following and 1 following) as next_field_value from rnt_agrs ra inner join rnt_agr_inv_notes rain on ra.rnt_agr_nbr=rain.rea_rnt_agr_nbr inner join RNT_CUST_ADDNL_INFO rcai on rain.rea_rnt_agr_nbr=rcai.rea_rnt_agr_nbr and rain.bac_acc_id=rcai.bac_acct_id where ra.rnt_agr_nbr=128260511 qualify row_number() -- only the first row over (partition by ra.rnt_agr_nbr order by rcai.field_value) = 1 If there might be only a single row you need to add a COALESCE(min...,'') to get rid of the NULL. Both OLAP functions specify the same PARTITION and ORDER, so this is a single working step.
select *,(SELECT top 2 rcai.field_value + ' AND ' AS [text()] FROM RNT_CUST_ADDNL_INFO rcai WHERE rcai.rea_rnt_agr_nbr = rain.rea_rnt_agr_nbr AND rcai.bac_acct_id=rain.bac_acc_id FOR XML PATH('')) AS Notes from rnt_agrs ra inner join rnt_agr_inv_notes rain on ra.rnt_agr_nbr=rain.rea_rnt_agr_nbr I had something like this, where there was a 1 to many, and I wanted a semicolon delimited set of values in a single column with the main record.
You could use PIVOT to transform the two note rows into two note columns based on row number, then concatenate them. Here's an example: SELECT pvt.[1] + ' and ' + pvt.[2] FROM ( --the selection of your table data, including a row-number column SELECT Msg, ROW_NUMBER() OVER(ORDER BY Id) --sample data shown here, but this would be your real table FROM (VALUES(1, 'Note 1'), (2, 'Note 2'), (3, 'Note 3')) Note(Id, Msg) ) Data (Msg, Row) PIVOT (MAX(Msg) FOR Row IN ([1], [2])) pvt Note that MAX is used for the aggregate in the PIVOT since an aggregate is required, but since ROW_NUMBER is unique, you're only aggregating a single value. This could also be easily extended to the first N rows - just include the row numbers you want in the pivot and combine them as desired in the select statement.
How can I select the Nth row of a group of fields?
I have a very very small database that I am needing to return a field from a specific row. My table looks like this (simplified) Material_Reading Table pointID Material_Name 123 WoodFloor 456 Carpet 789 Drywall 111 Drywall 222 Carpet I need to be able to group these together and see the different kinds (WoodFloor, Carpet, and Drywall) and need to be able to select which one I want and have that returned. So my select statement would put the various different types in a list and then I could have a variable which would select one of the rows - 1, 2, 3 for example. I hope that makes sense, this is somewhat a non-standard implementation because its a filemaker database unfortunately, so itstead of one big SQL statement doing all I need I will have several that will each select an individual row that I indicate. What I have tried so far: SELECT DISTINCT Material_Name FROM MATERIAL_READING WHERE Room_KF = $roomVariable This works and returns a list of all my material names which are in the room indicated by the room variable. But I cant get a specific one by supplying a row number. I have tried using LIMIT 1 OFFSET 1. Possibly not supported by Filemaker or I am doing it wrong, I tried it like this - it gives an error: SELECT DISTINCT Material_Name FROM MATERIAL_READING WHERE _Room_KF = $roomVariable ORDER BY Material_Name LIMIT 1 OFFSET 1 I am able to use ORDER BY like this: SELECT DISTINCT Material_Name FROM MATERIAL_READING WHERE Room_KF = $roomVariable ORDER BY Material_Name
In MSSQL SELECT DISTINCT Material_Name FROM MATERIAL_READING WHERE _Room_KF = 'roomVariable' ORDER BY Material_Name OFFSET N ROWS FETCH NEXT 5 ROWS ONLY where N->from which row does to start X->no.of rows to retrieve which were started from (N+1 row)
How to group by a column
Hi I know how to use the group by clause for sql. I am not sure how to explain this so Ill draw some charts. Here is my original data: Name Location ---------------------- user1 1 user1 9 user1 3 user2 1 user2 10 user3 97 Here is the output I need Name Location ---------------------- user1 1 9 3 user2 1 10 user3 97 Is this even possible?
The normal method for this is to handle it in the presentation layer, not the database layer. Reasons: The Name field is a property of that data row If you leave the Name out, how do you know what Location goes with which name? You are implicitly relying on the order of the data, which in SQL is a very bad practice (since there is no inherent ordering to the returned data) Any solution will need to involve a cursor or a loop, which is not what SQL is optimized for - it likes working in SETS not on individual rows
Hope this helps SELECT A.FINAL_NAME, A.LOCATION FROM (SELECT DISTINCT DECODE((LAG(YT.NAME, 1) OVER(ORDER BY YT.NAME)), YT.NAME, NULL, YT.NAME) AS FINAL_NAME, YT.NAME, YT.LOCATION FROM YOUR_TABLE_7 YT) A As Jirka correctly pointed out, I was using the Outer select, distinct and raw Name unnecessarily. My mistake was that as I used DISTINCT , I got the resulted sorted like 1 1 2 user2 1 3 user3 97 4 user1 1 5 3 6 9 7 10 I wanted to avoid output like this. Hence I added the raw id and outer select However , removing the DISTINCT solves the problem. Hence only this much is enough SELECT DECODE((LAG(YT.NAME, 1) OVER(ORDER BY YT.NAME)), YT.NAME, NULL, YT.NAME) AS FINAL_NAME, YT.LOCATION FROM SO_BUFFER_TABLE_7 YT Thanks Jirka
If you're using straight SQL*Plus to make your report (don't laugh, you can do some pretty cool stuff with it), you can do this with the BREAK command: SQL> break on name SQL> WITH q AS ( SELECT 'user1' NAME, 1 LOCATION FROM dual UNION ALL SELECT 'user1', 9 FROM dual UNION ALL SELECT 'user1', 3 FROM dual UNION ALL SELECT 'user2', 1 FROM dual UNION ALL SELECT 'user2', 10 FROM dual UNION ALL SELECT 'user3', 97 FROM dual ) SELECT NAME,LOCATION FROM q ORDER BY name; NAME LOCATION ----- ---------- user1 1 9 3 user2 1 10 user3 97 6 rows selected. SQL>
I cannot but agree with the other commenters that this kind of problem does not look like it should ever be solved using SQL, but let us face it anyway. SELECT CASE main.name WHERE preceding_id IS NULL THEN main.name ELSE null END, main.location FROM mytable main LEFT JOIN mytable preceding ON main.name = preceding.name AND MIN(preceding.id) < main.id GROUP BY main.id, main.name, main.location, preceding.name ORDER BY main.id The GROUP BY clause is not responsible for the grouping job, at least not directly. In the first approximation, an outer join to the same table (LEFT JOIN below) can be used to determine on which row a particular value occurs for the first time. This is what we are after. This assumes that there are some unique id values that make it possible to arbitrarily order all the records. (The ORDER BY clause does NOT do this; it orders the output, not the input of the whole computation, but it is still necessary to make sure that the output is presented correctly, because the remaining SQL does not imply any particular order of processing.) As you can see, there is still a GROUP BY clause in the SQL, but with a perhaps unexpected purpose. Its job is to "undo" a side effect of the LEFT JOIN, which is duplication of all main records that have many "preceding" ( = successfully joined) records. This is quite normal with GROUP BY. The typical effect of a GROUP BY clause is a reduction of the number of records; and impossibility to query or test columns NOT listed in the GROUP BY clause, except through aggregate functions like COUNT, MIN, MAX, or SUM. This is because these columns really represent "groups of values" due to the GROUP BY, not just specific values.
If you are using SQL*Plus, use the BREAK function. In this case, break on NAME. If you are using another reporting tool, you may be able to compare the "name" field to the previous record and suppress printing when they are equal.
If you use GROUP BY, output rows are sorted according to the GROUP BY columns as if you had an ORDER BY for the same columns. To avoid the overhead of sorting that GROUP BY produces, add ORDER BY NULL: SELECT a, COUNT(b) FROM test_table GROUP BY a ORDER BY NULL; Relying on implicit GROUP BY sorting in MySQL 5.6 is deprecated. To achieve a specific sort order of grouped results, it is preferable to use an explicit ORDER BY clause. GROUP BY sorting is a MySQL extension that may change in a future release; for example, to make it possible for the optimizer to order groupings in whatever manner it deems most efficient and to avoid the sorting overhead. For full information - http://academy.comingweek.com/sql-groupby-clause/
SQL GROUP BY STATEMENT SQL GROUP BY clause is used in collaboration with the SELECT statement to arrange identical data into groups. Syntax: 1. SELECT column_nm, aggregate_function(column_nm) FROM table_nm WHERE column_nm operator value GROUP BY column_nm; Example : To understand the GROUP BY clauserefer the sample database.Below table showing fields from “order” table: 1. |EMPORD_ID|employee1ID|customerID|shippers_ID| Below table showing fields from “shipper” table: 1. | shippers_ID| shippers_Name | Below table showing fields from “table_emp1” table: 1. | employee1ID| first1_nm | last1_nm | Example : To find the number of orders sent by each shipper. 1. SELECT shipper.shippers_Name, COUNT (orders.EMPORD_ID) AS No_of_orders FROM orders LEFT JOIN shipper ON orders.shippers_ID = shipper.shippers_ID GROUP BY shippers_Name; 1. | shippers_Name | No_of_orders | Example : To use GROUP BY statement on more than one column. 1. SELECT shipper.shippers_Name, table_emp1.last1_nm, COUNT (orders.EMPORD_ID) AS No_of_orders FROM ((orders INNER JOIN shipper ON orders.shippers_ID=shipper.shippers_ID) INNER JOIN table_emp1 ON orders.employee1ID = table_emp1.employee1ID) 2. GROUP BY shippers_Name,last1_nm; | shippers_Name | last1_nm |No_of_orders | for more clarification refer my link http://academy.comingweek.com/sql-groupby-clause/