SQL Query to select the COUNT from another SQL query - sql

I'm having a little trouble making an SQL SERVER 2000 query. Here is my scenario:
I have a table called Folders with 3 columns: pk_folderID, folderName and fk_userID.
Also, I have another table called FolderMedia which stores what media (whatever) belong to a certain folder. There are 2 columns: fk_folderID, fk_media.
And the last, I have a table called Media which stores some media details. It has a primary key pk_media and among other columns, it has a MediaType column which tells the type of that media: image or video.
Now, I would like a query that does the following:
Select all folders that belong to a certain fk_userID, and then also get the number of media in that folder. I've seen a query like this here on StackOverflow, but I didn't manage to upgrade it to get 2 counts of media (based on their type)
Basically, get the folder details (name, etc) for all folders that belong to a user(fk_userID) and also, for each folder get the number of images and videos in it (as separate values).
The select would basically return:
folderName, count(images in folder), count(videos in folder), other folder details.
One obvious solution would be to just get all folders and then manually calculate the number of images/videos in them... but I would first like to try with a query.
Thank you,

Basically something like this:
SELECT
f.pk_folderID,
f.folderName,
VideoCount = COUNT(CASE m.MediaType WHEN 'Video' THEN 1 END),
ImageCount = COUNT(CASE m.MediaType WHEN 'Image' THEN 1 END)
FROM Folder f
LEFT JOIN FolderMedia fm ON f.pk_folderID = fm.fk_folderID
LEFT JOIN Media m ON fm.fk_media = m.pk_media
WHERE f.fk_userID = #UserID
GROUP BY
f.pk_folderID,
f.folderName
UPDATE (based on the additional request):
To include a sort of TOP 1 Media.Name into the result set, the above query could be changed like this:
SELECT
f.pk_folderID,
f.folderName,
VideoCount = COUNT(CASE m.MediaType WHEN 'Video' THEN 1 END),
ImageCount = COUNT(CASE m.MediaType WHEN 'Image' THEN 1 END),
MediaName = MAX(CASE fm.timestamp WHEN t.timestamp THEN m.Name END)
FROM Folder f
LEFT JOIN FolderMedia fm ON f.pk_folderID = fm.fk_folderID
LEFT JOIN Media m ON fm.fk_media = m.pk_media
LEFT JOIN (
SELECT
fk_folderID,
timestamp = MIN(timestamp)
FROM FolderMedia
GROUP BY fk_folderID
) t ON fm.fk_folderID = t.fk_folderID AND fm.timestamp = t.timestamp
WHERE f.fk_userID = #UserID
GROUP BY
f.pk_folderID,
f.folderName
In cases where minimal FolderMedia.timestamp values are not unique within their folders, the ultimate value of the corresponding Media.Name will be decided by its alphabetical sorting. In particular, the above query selects the last one of the set (with MAX()).

Get all data you need from Folders table left join it with FolderMedia and Media
use sum with case inside to count all videos and images.
SUM(CASE WHEN mediaTypeId = videoId THEN 1 ELSE 0 END) as videoCount

//select folderName ,count(case when Folders.pk_folderID then 1 else null end)
count(SELECT FolderMedia.fk_media from FolderMedia JOIN Media on fk_media=pk_media where image is not null) as nrImg
etc..

Here is a query with some example data. Hope it helps.
declare #Folders table (pk_folderID int, folderName varchar(32), fk_userID int)
declare #Media table (pk_media int, name varchar(50), type varchar(32))
declare #FolderMedia table (fk_folderID int, fk_media int)
insert into #Folders values (1, 'Folder1', 1000)
insert into #Folders values (2, 'Folder2', 1000)
insert into #Folders values (3, 'Folder1', 2000)
insert into #Folders values (4, 'Folder1', 2000)
insert into #Media values (1, 'graph.jpg', 'image')
insert into #Media values (2, 'timer.jpg', 'image')
insert into #Media values (3, 'timer1.jpg', 'image')
insert into #Media values (4, 'harry_potter.mpeg', 'video')
insert into #Media values (5, 'harry_potter1.mpeg', 'video')
insert into #Media values (6, 'harry_potter2.mpeg', 'video')
insert into #FolderMedia values (1, 1)
insert into #FolderMedia values (1, 3)
insert into #FolderMedia values (1, 6)
insert into #FolderMedia values (2, 2)
insert into #FolderMedia values (2, 4)
select folderName, fk_userID, imageData.imgCount, videoData.videoCount from
#Folders
left outer join
(
select fk_folderID, COUNT(*) as imgCount
from #FolderMedia
inner join #Media
on fk_media = pk_media
and type = 'image'
group by fk_folderID
) as imageData
on imageData.fk_folderID = pk_folderID
left outer join
(
select fk_folderID, COUNT(*) as videoCount
from #FolderMedia
inner join #Media
on fk_media = pk_media
and type = 'video'
group by fk_folderID
) as videoData
on videoData.fk_folderID = pk_folderID
where fk_userID = 1000

Related

How to make an OUTER JOIN return ZERO instead of NULL

I am trying to accomplish this on SQL Server. The simplest table structure with data is shown below.
Table:Blog
BlogID, Title
----------------
1, FirstBlog
23, Pizza
Table:User
UserID, Name
-------------------
123, james
444, John
Table:UserBlogMapping
UserBlogMappingID, BlogID,UserID
----------------------------------
1, 1, 123
I want to get FormID and UserBlogMappingID in one SQL query. If provided UserID is not in the mapping table, return ZERO otherwise return the valid userBlogMappingID. I am trying to run the below query but its not correct.
SELECT
B.BlogID,
BUM.BlogUserMappingID
FROM
Blog AS B
LEFT JOIN BlogUserMapping AS BUM ON B.BlogID = BUM.BlogID
WHERE
(B.BlogID = 23) -- it exists in the table
AND BUM.userID = 444 -- it is NOT in the mmaping table but i want a ZERO return in such case
Assumption:
We can assume that the UserID provided in the WHERE clause is always valid UserID and is present in the User table.
You could put the criteria for the userID=444 in the ON clause of the LEFT JOIN.
And an ISNULL or a COALESCE to change a NULL to a 0.
Example using table variables:
declare #Blog table (BlogID int, Title varchar(30));
insert into #Blog (BlogId, Title) values
(1, 'FirstBlog'),
(23, 'Pizza');
declare #User table (UserID int, Name varchar(30));
insert into #User (UserID, Name) values
(123,'james'),
(444,'John');
declare #BlogUserMapping table (BlogUserMappingID int, BlogID int, UserID int);
insert into #BlogUserMapping (BlogUserMappingID, BlogID, UserID) values
(1, 1, 123),
(2, 23, 123),
(3, 1, 444);
-- Using the criteria in ON clause of the LEFT JOIN
SELECT
B.BlogID,
ISNULL(BUM.BlogUserMappingID,0) as BlogUserMappingID
FROM #Blog B
LEFT JOIN #BlogUserMapping BUM ON (B.BlogID = BUM.BlogID AND BUM.userID = 444)
WHERE B.BlogID = 23;
-- If there are more BlogId=23 with userID=444.
-- But only 1 row needs to be returned then you could also GROUP BY and take the maximum BlogUserMappingID
SELECT
B.BlogID,
MAX(ISNULL(BUM.BlogUserMappingID,0)) as BlogUserMappingID
FROM #Blog B
LEFT JOIN #BlogUserMapping BUM ON (B.BlogID = BUM.BlogID AND BUM.userID = 444)
WHERE B.BlogID = 23
GROUP BY B.BlogID;
-- Using an OR in the WHERE clause would also return a 0.
-- But it would also return nothing if the mapping table has a BlogID=23 with a userID<>444.
-- So not usefull in this case.
SELECT
B.BlogID,
ISNULL(BUM.BlogUserMappingID,0) as BlogUserMappingID
FROM #Blog B
LEFT JOIN #BlogUserMapping BUM ON B.BlogID = BUM.BlogID
WHERE B.BlogID = 23
AND (BUM.userID IS NULL OR BUM.userID = 444);

Dynamically update table with column from another table

I have a table customer like this:
CREATE TABLE tbl_customer (
id INTEGER,
name VARCHAR(16),
voucher VARCHAR(16)
);
and a voucher table like this:
CREATE TABLE tbl_voucher (
id INTEGER,
code VARCHAR(16)
);
Now imagine that the customer table always has rows with id and name filled in, however the voucher needs to be inserted periodically from the tbl_voucher table.
Important: every voucher may only be assigned to one specific customer (i.e. must be unique)
I wrote a query like this:
UPDATE tbl_customer
SET voucher = (
SELECT code
FROM tbl_voucher
WHERE code NOT IN (
SELECT voucher
FROM tbl_customer
WHERE voucher IS NOT NULL
)
LIMIT 1
)
WHERE voucher IS NULL;
However this is not working as expected, since the part that looks for an unused voucher is executed once and said voucher is then applied to every customer.
Any ideas on how I can solve this without using programming structures such as loops?
Also, some example data so you can imagine what I would like to happen:
INSERT INTO tbl_customer VALUES (1, 'Sara', 'ABC');
INSERT INTO tbl_customer VALUES (1, 'Simon', 'DEF');
INSERT INTO tbl_customer VALUES (1, 'Andy', NULL);
INSERT INTO tbl_customer VALUES (1, 'Alice', NULL);
INSERT INTO tbl_voucher VALUES (1, 'ABC');
INSERT INTO tbl_voucher VALUES (2, 'LOL');
INSERT INTO tbl_voucher VALUES (3, 'ZZZ');
INSERT INTO tbl_voucher VALUES (4, 'BBB');
INSERT INTO tbl_voucher VALUES (5, 'CCC');
After the wanted query is executed, I'd expect Andy to have the voucher LOL and Alice should get ZZZ
I am going to guess this is MySQL. The answer is that this is a pain. The following assigns the values in a select:
select c.*, v.voucher
from (select c.*, (#rnc := #rnc + 1) as rn
from tbl_customer c cross join
(select #rnc := 0) params
where c.voucher is null
) c join
(select v.*, (#rnv := #rnv + 1) as rn
from tbl_vouchers v cross join
(select #rnv := 0) params
where not exists (select 1 from tbl_customers c where c.voucher = v.voucher)
) v
on c.rn = v.rn;
You can now use this for the update:
update tbl_customer c join
(select c.*, v.voucher
from (select c.*, (#rnc := #rnc + 1) as rn
from tbl_customer c cross join
(select #rnc := 0) params
where c.voucher is null
) c join
(select v.*, (#rnv := #rnv + 1) as rn
from tbl_vouchers v cross join
(select #rnv := 0) params
where not exists (select 1 from tbl_customers c where c.voucher = v.voucher)
) v
on c.rn = v.rn
) cv
on c.id = cv.id
set c.voucher = cv.voucher;

Counting rows of a subgroup while ignoring duplicates

I can't find a way to describe my problem in an abstract and general manner, so I'll just provide a minimal example:
Let's say I have these 3 simple tables:
CREATE TABLE Document(
[Id] int IDENTITY(1, 1) NOT NULL PRIMARY KEY,
[Title] nvarchar(MAX),
[Patient] nvarchar(MAX)
);
CREATE TABLE Link(
DocumentId INT FOREIGN KEY REFERENCES Document(Id),
Text nvarchar(max)
);
CREATE TABLE ReadStatus(
DocumentId INT FOREIGN KEY REFERENCES Document(Id),
IsRead Bit NOT NULL,
UserId Int NOT NULL
);
We have a set of documents
A document can have 0 or more links
Documents can be read by users - this is tracked by the ReadStatus table, which associates a user with a document, and where IsRead=1 means the document has been read by that user and IsRead=0 means it hasn't been read by that user yet.
If, for document X and user A, a row does not exist in the ReadStatus table, we assume User A hasn't read document X yet.
Now, I need to run a query to select all patients. For each patient, I need the total number of documents available AND the number of documents that have already been read (i.e. IsRead=1). This is what I have so far:
SELECT d.Patient,
COUNT(DISTINCT d.Id) AS DocumentCount,
COUNT(NULLIF(rs.IsRead,0)) AS ReadDocumentCount,
COUNT(*) OVER () AS TotalPatientCount
FROM Document d
LEFT OUTER JOIN ReadStatus AS rs ON d.Id = rs.DocumentId AND rs.UserId = 123
INNER JOIN Link AS l ON d.Id = l.DocumentId AND l.Text IN ('Link W', 'Link X', 'Link T', 'Link Z')
GROUP BY d.Patient
The problem happens when a document (that has already been read) has more than one link. If that document has 3 links, the cartesian product produced by the INNER JOIN with the Link table will cause the ReadDocumentCount selection to be 3 instead of 1.
In other words, given this data:
INSERT INTO Document(Title, Patient) VALUES('Doc A', 'Mike')
INSERT INTO Document(Title, Patient) VALUES('Doc B', 'Mike')
INSERT INTO Link(DocumentId, Text) VALUES(1, N'Link W')
INSERT INTO Link(DocumentId, Text) VALUES(1, N'Link X')
INSERT INTO Link(DocumentId, Text) VALUES(1, N'Link Y')
INSERT INTO Link(DocumentId, Text) VALUES(2, N'Link Z')
INSERT INTO ReadStatus(DocumentID, IsRead, UserId) VALUES(1, 1, 123)
INSERT INTO ReadStatus(DocumentID, IsRead, UserId) VALUES(2, 0, 123)
I'm getting this as a result:
Patient DocumentCount ReadDocumentCount TotalPatientCount
Mike 2 3 1
Whereas this is what I want:
Patient DocumentCount ReadDocumentCount TotalPatientCount
Mike 2 1 1
SQL fiddle: http://sqlfiddle.com/#!6/e06bf/3
You can use COUNT(DISTINCT) conditionally as well:
SELECT d.Patient,
COUNT(DISTINCT d.Id) AS DocumentCount,
COUNT(DISTINCT (CASE WHEN rs.IsRead <> 0 THEN d.id END)) AS ReadDocumentCount,
COUNT(*) OVER () AS TotalPatientCount
FROM Document d LEFT OUTER JOIN
ReadStatus rs
ON d.Id = rs.DocumentId AND rs.UserId = 123 INNER JOIN
Link l
ON d.Id = l.DocumentId AND l.Text IN ('Link W', 'Link X', 'Link T', 'Link Z')
GROUP BY d.Patient;

How to UPDATE pivoted table in SQL SERVER

I have flat table which I have to join using EAN attribute with my main table and update gid (id of my main table).
id attrib value gid
1 weight 10 NULL
1 ean 123123123112 NULL
1 color blue NULL
2 weight 5 NULL
2 ean 331231313123 NULL
I was trying to pivot ean rows into column, next join on ean both tables, and for this moment everything works great.
--update SideTable
--set gid = ab_id
select gid, ab_id
from SideTable
pivot (max (value) for attrib in ([EAN],[MPN])) as b
join MainTable as c
on c.ab_ean = b.EAN
where b.EAN !='' AND c.ab_archive = '0'
When I am selecting both id columns is okey, but when I am uncomment first lines and delete select whole table is set with first gid from my main table.
It have to set my main id into all attributes where ID where ean is matched from my main table.
I am sorry for my terrible english but I hope someone can help me, with that.
The reason your update does not work is that you don't have any link between your source and target for the update, although you reference sidetable in the FROM clause, this is effectively destroyed by the PIVOT function, leaving no link back to the instance of SideTable that you are updating. Since there is no link, all rows are updated with the same value, this will be the last value encountered in the FROM.
This can be demonstrated by running the following:
DECLARE #S TABLE (ID INT, Attrib VARCHAR(50), Value VARCHAR(50), gid INT);
INSERT #S
VALUES
(1, 'weight', '10', NULL), (1, 'ean', '123123123112', NULL), (1, 'color', 'blue', NULL),
(2, 'weight', '5', NULL), (2, 'ean', '331231313123', NULL);
SELECT s.*
FROM #S AS s
PIVOT (MAX(Value) FOR attrib IN ([EAN],[MPN])) AS pvt;
You clearly have a table aliased s in the FROM clause, however because you have used pivot you cannot use SELECT s*, you get the following error:
The column prefix 's' does not match with a table name or alias name used in the query.
You haven't provided sample data for your main table, but I am about 95% certain your PIVOT is not needed, I think you can get your update using just normal JOINs:
UPDATE s
SET gid = ab_id
FROM SideTable AS s
INNER JOIN SideTable AS ean
ON ean.ID = s.ID
AND ean.attrib = 'ean'
INNER JOIN MainTable AS m
ON m.ab_EAN = ean.Value
WHERE m.ab_archive = '0'
AND m.ab_EAN != '';
As per comment to the question, you need to use update + select statement.
A standard version looks like:
UPDATE
T
SET
T.col1 = OT.col1,
T.col2 = OT.col2
FROM
Some_Table T
INNER JOIN
Other_Table OT
ON
T.id = OT.id
WHERE
T.col3 = 'cool'
As to your needs:
update a
set a.gid = p.ab_id
from SideTable As a
Inner join (
select gid, ab_id
from SideTable
pivot (max (value) for attrib in ([EAN],[MPN])) as b
join MainTable as c
on c.ab_ean = b.EAN
where b.EAN !='' AND c.ab_archive = '0') p ON a.ean = p.EAN
try and break it down a bit more like this..
update SideTable
set SideTable.gid = p.ab_id
FROM
(
select gid, ab_id
from SideTable
pivot (max (value) for attrib in ([EAN],[MPN])) as b
join MainTable as c
on c.ab_ean = b.EAN
where b.EAN !='' AND c.ab_archive = '0'
) p
WHERE p.EAN = SideTable.EAN

Selecting table of properties as columns

I have two tables, things and properties:
CREATE TABLE things (
id SERIAL PRIMARY KEY
);
CREATE TABLE properties (
thing_id INT,
key TEXT,
value TEXT
);
I want to select from things and join rows from properties as columns. For example, say I have the following:
INSERT INTO things DEFAULT_VALUES; -- suppose id is 1
INSERT INTO properties (thing_id, key, value) VALUES
(1, 'height', '5'),
(1, 'width', '6'),
(1, 'length', '7');
How can I select from things with height, width, and length as columns?
Also, I don't want to specifically select height, width, and length, but any rows that may be inside properties.
For just three columns:
SELECT t.thing_id
,max(CASE WHEN p.key = 'height' THEN p.value END) AS height
,max(CASE WHEN p.key = 'width' THEN p.value END) AS width
,max(CASE WHEN p.key = 'length' THEN p.value END) AS length
FROM things t
LEFT JOIN properties p ON p.thing_id = t.id
WHERE t.id = 1
GROUP BY 1;
Or use crosstab() from the additional module tablefunc which is typically faster, and shorter for long lists of attributes:
SELECT * FROM crosstab(
'SELECT t.thing_id, p.key, p.value
FROM things t
LEFT JOIN properties p ON p.thing_id = t.id
WHERE t.id = 1
ORDER BY 1'
,$$VALUES ('height'::text), ('width'), ('length')$$) -- add more
AS ct (thing_id int, height int, width int, length int); -- add more
Types have to match. Detailed explanation:
PostgreSQL Crosstab Query
A completely dynamic list of columns cannot be achieved in a single query. I have tried many times. Here is what can be done:
Dynamic alternative to pivot with CASE and GROUP BY
May be you can try table aliases here:
SELECT p1.key, p2.key, p3.key
FROM properties as t1
JOIN properties AS p1 ON p1.thing_id= t1.thing_id
JOIN properties AS p2 ON p2.thing_id= t1.thing_id
JOIN properties AS p3 ON p3.thing_id= t1.thing_id
WHERE t1.thing_id = 1;