SQL: Using Multi-Row data in Column - sql-server-2005

This may not be possible in one query, but I'd like to see what my options are.
I have a large query that returns the data for each piece of an inventory (in this case trees). The query gets data from a few different tables. I mostly use left outer joins to bring this information in, so if it's not there I can ignore it and take the NULL. I have an interesting situation where a one-to-many relationship exists between "tree" and it's "pests".
tree table: treeID, treeHeight, etc....
pest (pest to tree) table: pestID, treeID, pestRef.....
I need a query that gets the top 6 pests for each tree and returns them as columns:
pest1, pest2, pest3... and so on.
I know that I could do this in multiple queries, however that would happen thousands of times just per use and our servers can't handle that.
Some notes: we're using ColdFusionMX7, and my knowledge of stored procedures is very low.

One approach is to generate a column representing the pest rank by tree, then join the ranked pest table to the tree table with rank as a join condition. Make sure you use ROW_NUMBER not RANK because a tie would cause repeated numbers in RANK (but not ROW_NUMBER), and make sure you use LEFT OUTER joins so trees with fewer pests are not excluded. Also, I ordered by 2 conditions, but anything valid in a normal ORDER BY clause is valid here.
DECLARE #t TABLE (TreeID INT, TreeName VARCHAR(25));
DECLARE #p TABLE (PestID INT, TreeID INT, PestName VARCHAR(25));
INSERT INTO #t VALUES (1,'ash'),(2,'elm'),(3,'oak')
INSERT INTO #p VALUES (1,1,'ash borer'),(2,1,'tent catapilar'),(3,1,'black weevil'),(4,1,'brown weevil');
INSERT INTO #p VALUES (5,2,'elm thrip'),(6,2,'wooly adelgid');
INSERT INTO #p VALUES (7,3,'oak gall wasp'),(8,3,'asian longhorn beetle'),(9,3,'aphids');
WITH cteRankedPests as (
SELECT PestID, TreeID, PestName, ROW_NUMBER() OVER (PARTITION BY TreeID ORDER BY PestName,PestID) as PestRank
FROM #p
)
SELECT T.TreeID, T.TreeName
, P1.PestID as P1ID, P1.PestName as P1Name
, P2.PestID as P2ID, P2.PestName as P2Name
, P3.PestID as P3ID, P3.PestName as P3Name
FROM #t as T
LEFT OUTER JOIN cteRankedPests as P1 ON T.TreeID = P1.TreeID AND P1.PestRank = 1
LEFT OUTER JOIN cteRankedPests as P2 ON T.TreeID = P2.TreeID AND P2.PestRank = 2
LEFT OUTER JOIN cteRankedPests as P3 ON T.TreeID = P3.TreeID AND P3.PestRank = 3

Related

Create New SQL Table w/o duplicates

I'm learning how to create tables in SQL pulling data from existing tables from two different databases. I am trying to create a table combining two tables without duplicates. I've seen some say using UNION but I could not get that to work.
Say TABLE 1 has 2 COLUMNS (IdNumber, Material) and TABLE 2 has 3 COLUMNS (IdNumber, Size, Description)
How can I create a new table (named TABLE3) that combines those two but only shows the columns (PartDescription, Weight, Color) but without duplicates.
What I have done so far is as follows:
CREATE TABLE #Materialsearch (IdNumber varchar(30), Material varchar(30))
CREATE TABLE #Sizesearch (idnumber varchar(30), Size varchar(30), Description varchar(50))
INSERT INTO #Materialsearch (IdNumber, Material)
SELECT [IdNumber],[Material]
FROM [datalist].[dbo].[Table1]
WHERE Material LIKE 'Steel' AND IdNumber NOT LIKE 'Steel'
INSERT INTO #Sizesearch (idnumber, Size, Description)
SELECT [idNumber],[itemSize], [ShortDesc]
FROM [515dap].[dbo].[Table2]
WHERE itemSize LIKE '1' AND idnumber NOT LIKE 'Steel'
SELECT DISTINCT #Materialsearch.IdNumber, #Materialsearch.Material,
#Sizesearch.Size, #Sizesearch.Description
FROM #Materialsearch
INNER JOIN #Sizesearch
ON #Materialsearch.IdNumber = #Sizesearch.idnumber
ORDER BY #Materialsearch.IdNumber
DROP TABLE #Materialsearch
DROP TABLE #Sizesearch
This would show all items that are made from steel but do not have steel as their itemid's.
Thanks for your help
I'm not 100% sure what you're after - but you may find this useful.
You could use a FULL OUTER JOIN which takes takes all rows from both tables, matches the ones it can, then reports all rows.
I'd suggest (for your understanding) running
SELECT A.*, B.*
FROM #Materialsearch AS A
FULL OUTER JOIN #Sizesearch AS B ON A.[IdNumber] = B.[IdNumber]
Then to get the relevant data, you just need some tweaks on that e.g.,
SELECT
ISNULL(A.[IdNumber], B.[IdNumber]) AS [IdNumber],
A.Material,
B.Size,
B.Description
FROM #Materialsearch AS A
FULL OUTER JOIN #Sizesearch AS B ON A.[IdNumber] = B.[IdNumber]
Edit: Changed typoed INNER JOINs to FULL OUTER JOINs. Oops :( Thankyou very much #Thorsten for finding it!

SQL Queries instead of Cursors

I'm creating a database for a hypothetical video rental store.
All I need to do is a procedure that check the availabilty of a specific movie (obviously the movie can have several copies). So I have to check if there is a copy available for the rent, and take the number of the copy (because it'll affect other trigger later..).
I already did everything with the cursors and it works very well actually, but I need (i.e. "must") to do it without using cursors but just using "pure sql" (i.e. queries).
I'll explain briefly the scheme of my DB:
The tables that this procedure is going to use are 3: 'Copia Film' (Movie Copy) , 'Include' (Includes) , 'Noleggio' (Rent).
Copia Film Table has this attributes:
idCopia
Genere (FK references to Film)
Titolo (FK references to Film)
dataUscita (FK references to Film)
Include Table:
idNoleggio (FK references to Noleggio. Means idRent)
idCopia (FK references to Copia film. Means idCopy)
Noleggio Table:
idNoleggio (PK)
dataNoleggio (dateOfRent)
dataRestituzione (dateReturn)
dateRestituito (dateReturned)
CF (FK to Person)
Prezzo (price)
Every movie can have more than one copy.
Every copy can be available in two cases:
The copy ID is not present in the Include Table (that means that the specific copy has ever been rented)
The copy ID is present in the Include Table and the dataRestituito (dateReturned) is not null (that means that the specific copy has been rented but has already returned)
The query I've tried to do is the following and is not working at all:
SELECT COUNT(*)
FROM NOLEGGIO
WHERE dataNoleggio IS NOT NULL AND dataRestituito IS NOT NULL AND idNoleggio IN (
SELECT N.idNoleggio
FROM NOLEGGIO N JOIN INCLUDE I ON N.idNoleggio=I.idNoleggio
WHERE idCopia IN (
SELECT idCopia
FROM COPIA_FILM
WHERE titolo='Pulp Fiction')) -- Of course the title is just an example
Well, from the query above I can't figure if a copy of the movie selected is available or not AND I can't take the copy ID if a copy of the movie were available.
(If you want, I can paste the cursors lines that work properly)
------ USING THE 'WITH SOLUTION' ----
I modified a little bit your code to this
WITH film
as
(
SELECT idCopia,titolo
FROM COPIA_FILM
WHERE titolo = 'Pulp Fiction'
),
copy_info as
(
SELECT N.idNoleggio, N.dataNoleggio, N.dataRestituito, I.idCopia
FROM NOLEGGIO N JOIN INCLUDE I ON N.idNoleggio = I.idNoleggio
),
avl as
(
SELECT film.titolo, copy_info.idNoleggio, copy_info.dataNoleggio,
copy_film.dataRestituito,film.idCopia
FROM film LEFT OUTER JOIN copy_info
ON film.idCopia = copy_info.idCopia
)
SELECT COUNT(*),idCopia FROM avl
WHERE(dataRestituito IS NOT NULL OR idNoleggio IS NULL)
GROUP BY idCopia
As I said in the comment, this code works properly if I use it just in a query, but once I try to make a procedure from this, I got errors.
The problem is the final SELECT:
SELECT COUNT(*), idCopia INTO CNT,COPYFILM
FROM avl
WHERE (dataRestituito IS NOT NULL OR idNoleggio IS NULL)
GROUP BY idCopia
The error is:
ORA-01422: exact fetch returns more than requested number of rows
ORA-06512: at "VIDEO.PR_AVAILABILITY", line 9.
So it seems the Into clause is wrong because obviously the query returns more rows. What can I do ? I need to take the Copy ID (even just the first one on the list of rows) without using cursors.
You can try this -
WITH film
as
(
SELECT idCopia, titolo
FROM COPIA_FILM
WHERE titolo='Pulp Fiction'
),
copy_info as
(
select N.idNoleggio, I.dataNoleggio , I.dataRestituito , I.idCopia
FROM NOLEGGIO N JOIN INCLUDE I ON N.idNoleggio=I.idNoleggio
),
avl as
(
select film.titolo, copy_info.idNoleggio, copy_info.dataNoleggio,
copy_info.dataRestituito
from film LEFT OUTER JOIN copy_info
ON film.idCopia = copy_info.idCopia
)
select * from avl
where (dataRestituito IS NOT NULL OR idNoleggio IS NULL);
You should think in terms of sets, rather than records.
If you find the set of all the films that are out, you can exclude them from your stock, and the rest is rentable.
select copiafilm.* from #f copiafilm
left join
(
select idCopia from #r Noleggio
inner join #i include on Noleggio.idNoleggio = include.idNoleggio
where dateRestituito is null
) out
on copiafilm.idCopia = out.idCopia
where out.idCopia is null
I solved the problem editing the last query into this one:
SELECT COUNT(*),idCopia INTO CNT,idCopiaFilm
FROM avl
WHERE (dataRestituito IS NOT NULL OR idNoleggio IS NULL) AND rownum = 1
GROUP BY idCopia;
IF CNT > 0 THEN
-- FOUND AVAILABLE COPY
END IF;
EXCEPTION
WHEN NO_DATA_FOUND THEN
-- NOT FOUND AVAILABLE COPY
Thank you #Aditya Kakirde ! Your suggestion almost solved the problem.

Most efficient way of comparing two tables in SQL Server

So I have two tables which will store sales figures for products. Table one holds the last 6 weeks sales figures for each product and table 2 shows the last 12 months. I need to find a way to compare these two tables to then produce a 3rd table which will contain the difference between the 2 values as column 2 as well as the products Sage code in column one. What would be the most efficient (in terms of time) way to do this as there will be a fair amount of products to compare and it will only continue to grow? The product Sage code is the key identifier here. The two tables are created as below.
IF OBJECT_ID('tempdb..#Last6WeeksProductSales') IS NOT NULL DROP TABLE #Last6WeeksProductSales;
CREATE TABLE #Last6WeeksProductSales
(
CompoundSageCode varchar(200),
Value decimal(18,2)
)
INSERT INTO #Last6WeeksProductSales
SELECT [SalesOrderLine].[sProductSageCode] AS [CompoundSageCode],
SUM([SalesOrderLine].[fQtyOrdered] * [SalesOrderLine].[fPricePerUnit]) AS [Value]
FROM [SalesOrderLine]
INNER JOIN [SalesOrder] ON (SalesOrder.iSalesOrderID = SalesOrderLine.iSalesOrderID)
WHERE [SalesOrder].[dOrderDateTime] > DateAdd("ww", -6, CURRENT_TIMESTAMP)
GROUP BYsProductSageCode;
SELECT * FROM #Last6WeeksProductSales
ORDER BY CompoundSageCode;
IF OBJECT_ID('tempdb..#Last12MonthsProductSales') IS NOT NULL DROP TABLE #Last12MonthsProductSales;
CREATE TABLE #Last12MonthsProductSales
(
CompoundSageCode varchar(200),
Value decimal(18,2)
)
INSERT INTO #Last12MonthsProductSales SELECT [SalesOrderLine].[sProductSageCode] AS [CompoundSageCode],
SUM([SalesOrderLine].[fQtyOrdered] * [SalesOrderLine].[fPricePerUnit]) AS [Value]
FROM [SalesOrderLine]
INNER JOIN [SalesOrder] ON (SalesOrder.iSalesOrderID = SalesOrderLine.iSalesOrderID)
WHERE [SalesOrder].[dOrderDateTime] > DateAdd(month, -12, CURRENT_TIMESTAMP)
GROUP BY sProductSageCode;
SELECT * FROM#Last12MonthsProductSales
ORDER BY CompoundSageCode;
DROP TABLE #Last6WeeksProductSales;
DROP TABLE #Last12MonthsProductSales;
Use a view. That way you don't have to worry about updating your third table, and it will reflect current information. Base the view on a basic SELECT:
SELECT sixS.CompoundSageCode,
(twelveS.value - sixS.Value ) as diffValue
FROM Last6WeeksProductSales sixS
INNER JOIN Last12MonthsProductSales twelveS ON sixS.CompoundSageCode = twelveS.CompoundSageCode
(I have not tested this code, but it should be a good starting point.)
Computing the difference of two tables is usually done using a FULL OUTER JOIN. SQL Server can implement it using all three of the physical join operators. Apply reasonable indexing and it will run fine.
If you can manage it, create covering indexes on both tables that are sorted by the join key. This will result in a highly efficient merge join plan.

Placing different rows in succession

I've started working with access around 1 month ago and I'm actually making a tool for preventive medicine so they can use a digital version of their actual paper form.
While the program is nearly finished, the med who requested it now wants to export to excel (the easy part) all the data from a patient his treatment and all the medicines used during that treatment in a single line (the problem).
I've been beating my head over that for two days, trying and researching on google, but all i could find was how to put values from a column in a single cell, and that's not how it has to be displayed.
So far, my best attempt (which is far from a good one) has been something like that:
CREATE TABLE Patient
(`SIP` int, `name` varchar(10));
INSERT INTO Patient
(`SIP`, `name`)
VALUES
(70,'John');
-- A patient can have multiple treatments
CREATE TABLE Treatment
(`id` int, `SIPFK` int);
INSERT INTO Treatment
(`id`,`SIPFK`)
VALUES
(1,70);
-- A treatment can have multiple medicines used while it's open
CREATE TABLE Medicine
(`Id` int, `Name` varchar(8), `TreatFK` int);
INSERT INTO Medicine
(`Id`, `Name`, `TreatFK`)
VALUES
(7, 'Apples', 1),
(7, 'Tomatoes', 1),
(7, 'Potatoes', 1),
(8, 'Banana', 2),
(8, 'Peach', 2);
-- The query
select c.id, c.Name, p.id as id2, p.Name as name2, r.id as id3, r.Name as name3
from Medicine as c, Medicine as p, Medicine as r
where c.id = 7 and p.id=7 and r.id=7;
The output I was trying to get was:
7 | Apples | 7 | Tomatoes | 7 | Potatoes
The table medicines will have more columns than that and i need to show every row related to a treatment in a single row along with the treatment.
But the values keep repeating themselves on different rows and the output on the subsequent columns besides the first ones is not as expected. Also GROUP BY won't solve the problem and DISTINCT doesn't work.
The output of the query is as follows: sqlfiddle.com
If any one could give me a hint, I would be grateful.
EDIT: Since access is a derp and won't let me use any good SQL fix nor will recognize DISTINCT to make the data from the queries not repeat themselves, I will try and search for a way to organize the rows directly in the exported excel.
Thank you all for your help, I'll save it cause I'm sure it'll save me hours of hands in the head.
This is a bit problemation, because MS Access does not support recursive CTE's and I dont see a way of doing that without Ranking.
Hence, I have tried to reproduce the results by using subquery which ranks the Medicines
and store these into a temporary table.
create table newtable
select c.id
, c.Name
,(SELECT COUNT(T1.Name) FROM Medicine AS T1 WHERE t1.id=c.id and T1.Name >= c.Name) AS Rank
from Medicine as c;
Afterwards, it is easy because my query is mostly based on Ranks and IDs.
select distinct id
,(select Name from newtable t2 where t1.id=t2.id and rank=1) as firstMed
,(select Name from newtable t2 where t1.id=t2.id and rank=2) as secMed
,(select Name from newtable t2 where t1.id=t2.id and rank=3) as ThirdMed
from newtable t1;
According to me, the SELF JOIN concept and the notion of recursive CTE's are the most important points for that particular example and a good practice would be to do a resarch on these.
for reference: http://sqlfiddle.com/#!2/f80a9/2

Is it possible to report on 2 tables without using a subquery?

You have one table against which you wish to count the number of items in two different tables. In this example I used buildings, men and women
DROP TABLE IF EXISTS building;
DROP TABLE IF EXISTS men;
DROP TABLE IF EXISTS women;
CREATE TABLE building(name VARCHAR(255));
CREATE TABLE men(building VARCHAR(255), name VARCHAR(255));
CREATE TABLE women(building VARCHAR(255), name VARCHAR(255));
INSERT INTO building VALUES('building1');
INSERT INTO building VALUES('building2');
INSERT INTO building VALUES('building3');
INSERT INTO men VALUES('building1', 'andy');
INSERT INTO men VALUES('building1', 'barry');
INSERT INTO men VALUES('building2', 'calvin');
INSERT INTO men VALUES(null, 'dwain');
INSERT INTO women VALUES('building1', 'alice');
INSERT INTO women VALUES('building1', 'betty');
INSERT INTO women VALUES(null, 'casandra');
select
r1.building_name,
r1.men,
GROUP_CONCAT(women.name) as women,
COUNT(women.name) + r1.men_count as count
from
(select
building.name as building_name,
GROUP_CONCAT(men.name) as men,
COUNT(men.name) as men_count
from
building
left join
men on building.name=men.building
GROUP BY building.name) as r1
left join
women on r1.building_name=women.building
GROUP BY r1.building_name;
Might there be another way? The problem with the above approach is that the columns of the two tables in the subquery are hidden and need to be redeclared in the outer query. Doing it in two separate set operations creates an asymmetry where there is none. We could equally have joined to the women first and then the men.
In SQL Server, I would just join two subqueries with two left joins - if symmetry is what you are looking for:
SELECT *
FROM building
LEFT JOIN (SELECT building, etc. FROM men GROUP BY etc.) AS men_summary
ON building.name = men_summary.building_name
LEFT JOIN (SELECT building, etc. FROM women GROUP BY etc.) AS women_summary
ON building.name = women_summary.building_name
I tend to use common table expressions declared first instead of subqueries - it's far more readable (but not ANSI - but then neither is GROUP_CONCAT).
Use Union to combine the data from the men/women tables
select building, [name] as menname, null as womenname from men
union
select building, null as menname, [name] as womenname from women
you now have a single 'table' addmitedly in a subquery against which you can join, count or whatever.
BTW I can see why Cas[s]andra is out in the cold as no-one belives her, but what about dwain, is he similarly cursed by the gods?