Is it possible to select subqueries - sql

Consider the query below:
SELECT DISTINCT ON (ser.id) *
FROM server ser
LEFT JOIN subscription sub ON ser.id = sub.server_id
WHERE (
COUNT(SELECT err.id FROM error err WHERE ser.id = err.id) > 0
OR SUM(SELECT pay.amount FROM payment pay WHERE ser.id = pay.id) > 0
);
Here, a list of unique servers that are being subscribed to and that has errors or payments is returned.
However, instead of returning all server columns (*), I want to return the server id, the number of errors and the sum of payments. For example, the initial selection should look like this:
SELECT DISTINCT ON (ser.id) ser.id, countErrors, sumPayments
Selecting ser.id is straight forward, but how can countErrors and sumPayments be selected from the aggregate functions "count" and "sum" (considering that they are conditions in a WHERE clause)?
I imagined the "where" conditions would look something like this:
COUNT(SELECT err.id FROM error err WHERE ser.id = err.id) AS countErrors > 0
OR SUM(SELECT pay.amount FROM payment pay WHERE ser.id = pay.id) AS sumPayments > 0
Is it possible to do this? If so, how can it be achieved?
Test data is shown below:
server
+----+
| id |
+----+
| 1 |
+----+
| 2 |
+----+
| 3 |
+----+
| 4 |
+----+
subscription
+----+-----------+
| id | server_id |
+----+-----------+
| 1 | 1 |
+----+-----------+
| 2 | 2 |
+----+-----------+
| 3 | 2 |
+----+-----------+
| 4 | 3 |
+----+-----------+
| 5 | 3 |
+----+-----------+
error
+----+-----------+
| id | server_id |
+----+-----------+
| 1 | 1 |
+----+-----------+
| 3 | 4 |
+----+-----------+
payment
+----+-----------+--------+
| id | server_id | amount |
+----+-----------+--------+
| 1 | 1 | 200 |
+----+-----------+--------+
| 2 | 2 | 200 |
+----+-----------+--------+
| 3 | 2 | 100 |
+----+-----------+--------+
Wanted result from test data:
+-----------+-------------+-------------+
| server_id | countErrors | sumPayments |
+-----------+-------------+-------------+
| 1 | 1 | 200 |
+-----------+-------------+-------------+
| 2 | 0 | 300 |
+-----------+-------------+-------------+
Server#4 has no subscription, so it should be left out.
Server#3 has a subscription, but no errors or payments, so should be left out.
Server#1 and server#2 both have subscription and payments and/or errors.

Unless I'm missing something, I would just write your query as follows. Perform the aggregation of errors and payments in separate bona fide subqueries, and join to them. Also, there is a join to the subscription table, but this only exists to filter off servers having no subscription. Finally, the WHERE clause removes any servers which do not either have some errors or payments.
SELECT
s.id AS server_id,
COALESCE(e.countErrors, 0) AS countErrors,
COALESCE(p.sumPayments, 0) AS sumPayments
FROM server s
INNER JOIN
(
SELECT DISTINCT server_id
FROM subscription
) su
ON s.id = su.server_id
LEFT JOIN
(
SELECT server_id, COUNT(*) AS countErrors
FROM error
GROUP BY server_id
) e
ON s.id = e.server_id
LEFT JOIN
(
SELECT server_id, SUM(amount) AS sumPayments
FROM payment
GROUP BY server_id
) p
ON s.id = p.server_id
WHERE
p.sumPayments > 0 OR
e.countErrors > 0
ORDER BY
s.id;
Demo

The mistake here is to put the COUNT outside of the SELECT, it needs to go inside:
(SELECT COUNT(err.id) FROM error err WHERE ser.id = err.id) > 0
OR (SELECT SUM(pay.amount) FROM payment pay WHERE ser.id = pay.id) > 0

Related

What's the best way to create empty/default rows for missing aggregations

I have a table I want to group over two levels. As the output, I need all the grouping value combinations, such that I end up with zeros where non existant combinations occur. For example, say I have this table:
+------+------+
| user | page |
+------+------+
| a | 1 |
| a | 1 |
| a | 2 |
| b | 2 |
| b | 3 |
+------+------+
I'm after output like this:
+------+------+--------+
| user | page | visits |
+------+------+--------+
| a | 1 | 2 |
| a | 2 | 1 |
| a | 3 | 0 |
| b | 1 | 0 |
| b | 2 | 1 |
| b | 3 | 1 |
+------+------+--------+
I can achieve this with the following query, but it seems rather heavy handed:
WITH
users AS (SELECT distinct(user) FROM sometable),
pages AS (SELECT distinct(page) FROM sometable),
users_pages_empty AS (SELECT * FROM users CROSS JOIN pages),
users_pages_full AS (SELECT user, page, count(*) as visits FROM sometable GROUP BY user, page)
SELECT e.user, e.page, coalesce(f.visits, 0) as visits
FROM users_pages_empty e
LEFT JOIN users_pages_full f ON e.user=f.user AND e.page=f.page
I happen to be using AWS Athena, but I think this is more a generic SQL question than an Athena question.
The performance of this query is fine, it's more the readability/complexity I'm not happy with.
Use a cross join to generate the rows and a left join to bring in the existing rows and aggregate:
select u.user, p.page, count(s.user)
from (select distinct user from sometable) u cross join
(select distinct page from sometable) p left join
sometable s
on s.user = u.user and s.page = p.page
group by u.user, p.page
order by u.user, p.page;

SQL Server join where not exist on other table

+-------------------+ +-------------------+ +---------------------+
| Service | | Asset | | AssetService |
+-------------------+ +-------------------+ +---------------------+
| Id | Name | | Id | Name | | AssetId | ServiceId |
|-------------------| |-------------------| |---------------------|
| 1 | Service 1 | | 1 | Asset 1 | | 1 | 1 |
| 2 | Service 2 | | 2 | Asset 2 | | 1 | 2 |
| 3 | Service 3 | | 3 | Asset 3 | | 2 | 2 |
+-------------------+ +-------------------+ | 2 | 3 |
+---------------------+
So I have these tables. I want to get the Services that is not on AssetService where AssetId = 1
Like this:
+-------------------+
| Service |
| Id | Name |
+-------------------+
| 3 | Service 3 |
+-------------------+
Is this possible with just inner/left/right join? because I already tried different combinations of inner join but it's not working, like this inner join Asset a on a.Id != as.AssetId. I event tried left and right join.
Can somebody help me?
Thanks.
You can you use an intelligent left join to return non-matching rows only from left table(Service)
SELECT S.Id, S.Name FROM [Service] S
LEFT JOIN ServiceAsset SA
ON S.Id = SA.ServiceId
WHERE SA.ServiceId IS NULL
Note: INNER JOIN returns the matching rows whereas you want the non matching rows then use LEFT JOIN instead
The simplest I can think of:
select * from Service
where Id not in (
select ServiceId
from AssetService
where AssetId = 1);
SQLFiddle link
I don't think it's possible using inner join, because that would only retrieve records that match some criteria and you are looking for records that do not match.
It is, however, possible to do it with left join as Ctznkane525 shows in his answer.
Edit
As jarlh pointed out in the comments, not in might lead to surprising results when there are nulls in the subquery. So, here is the not exists version:
select Id, Name
from Service s
where not exists (
select *
from AssetService a
where AssetId = 1
and ServiceId = s.Id);
SQLFiddle link
Try this:
select * from Service where Id not in (
select ServiceId from AssetService where AssetId = 1
-- we have to filter out NULLs, in case of NULL values query result will be empty
and ServiceId not null
)
It doesn't require any join.
Here is solution with join:
select Id, Name from Service
except
select S.Id, S.Name from Service S join AssetService [AS] on S.Id = [AS].ServiceId
where [AS].AssetId = 1

how to bake in a record count in a sql query

I have a query that looks like this:
select id, extension, count(distinct(id)) from publicids group by id,extension;
This is what the results looks like:
id | extension | count
-------------+-------------------------+-------
18459154909 | 12333 | 1
18459154909 | 9891114 | 1
18459154919 | 43244 | 1
18459154919 | 8776232 | 1
18766145025 | 12311 | 1
18766145025 | 1122111 | 1
18766145201 | 12422 | 1
18766145201 | 14141 | 1
But what I really want is for the results to look like this:
id | extension | count
-------------+-------------------------+-------
18459154909 | 12333 | 2
18459154909 | 9891114 | 2
18459154919 | 43244 | 2
18459154919 | 8776232 | 2
18766145025 | 12311 | 2
18766145025 | 1122111 | 2
18766145201 | 12422 | 2
18766145201 | 14141 | 2
I'm trying to get the count field to show the total number of records that have the same id.
Any suggestions would be appreciated
I think you want to count distincts extentions, not ids.
Run this query:
select id
, extension
(select count(*) from publicids p1 where p.id = p1.id ) distinct_id_count
from publicids p
group by id,extension;
This is more or less the same as Pastor's answer. Depending on what the optimizer does it might be faster with higher record count source tables.
select p.id, p.extension, p2.id_count
from publicids p
inner join (
select id, count(*) as id_count
from publicids group by id
) as p2 on p.id = p2.id

Count within the result set of a subquery

I have the following relations in my database:
Invoice InvoiceMeal
--------------------- ---------------------------
| InvoiceId | Total | | Id | InvoiceId | MealId |
--------------------- ---------------------------
| 1 | 22.32 | | 1 | 1 | 3 |
--------------------- ---------------------------
| 2 | 12.18 | | 2 | 1 | 2 |
--------------------- ---------------------------
| 3 | 27.76 | | 3 | 2 | 2 |
--------------------- ---------------------------
Meal Type
----------------------------------- -------------------
| Id | Name | TypeId | | Id | Name |
----------------------------------- -------------------
| 1 | Hamburger | 1 | | 1 | Meat |
----------------------------------- -------------------
| 2 | Soja Beans | 2 | | 2 | Vegetarian |
----------------------------------- -------------------
| 3 | Chicken | 2 |
-----------------------------------
What I want to query from the database is InvoiceId and Total of all Invoices which consist of at least two Meals where at least one of the Meals is of Type Vegetarian. I have the following SQL query and it works:
SELECT
i."Id", i."Total"
FROM
public."Invoice" i
WHERE
(SELECT COUNT(*)
FROM public."InvoiceMeal" im
WHERE im."InvoiceId" = i."Id" AND
(SELECT COUNT(*)
FROM public."Meal" m, public."Type" t
WHERE im."MealId" = m."Id" AND
m."TypeId" = t."Id" AND
g."Name" = 'Vegetarian') > 0
) >= 2;
My problem with this query is that I can not easily modify the condition that there must at least one vegetarien Meal. I want to be able, for example, to change it to at least two vegetarian meals. How can I achieve this with my query?
I would approach this by joining the tables together and using aggregation. The having clause can handle the conditions:
select i.Id, i.Total
from InvoiceMeal im join
Invoice i
on i.InvoiceId = im.InvoiceId join
Meal m
on im.mealid = m.mealid join
Type t
on m.typeid = t.typeid
group by i.Id, i.Total
having count(distinct im.mealid) >= 2 and
sum(case when t.name = 'Vegetarian' then 1 else 0 end) > 0;
I also see no reason to put double quotes around column names. That just makes the query harder to write and read.

problem with Update in MySQL

According to the documentation, joins, when used with the update statement, work in the same way as when used in selects.
For example, if we have these two tables:
mysql> SELECT * FROM orders;
+---------+------------+
| orderid | customerid |
+---------+------------+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
| 4 | 1 |
+---------+------------+
mysql> SELECT * FROM customers;
+------------+------------+
| customerid | ordercount |
+------------+------------+
| 1 | 9 |
| 2 | 3 |
| 3 | 8 |
| 4 | 5 |
| 5 | 7 |
+------------+------------+
using this select statements:
SELECT orders.customerid
FROM orders
JOIN customers ON (customers.customerid = orders.customerid)
returns:
+------------+
| customerid |
+------------+
| 1 |
| 1 |
| 2 |
| 3 |
+------------+
So, I was expecting the statement below:
UPDATE orders
JOIN customers ON (customers.customerid = orders.customerid)
SET ordercount = ordercount + 1
to update ordercount for customer #1 (customerid = 1) to be 11, but actually this is not the case, here are the results after the update:
mysql> SELECT * FROM customers;
+------------+------------+
| customerid | ordercount |
+------------+------------+
| 1 | 10 |
| 2 | 4 |
| 3 | 9 |
| 4 | 5 |
| 5 | 7 |
+------------+------------+
As you can see it was only incremented once despite that it occurs twice in the orders table and despite that the select statement returns it correctly.
Is this a bug in MySQL or is it me doing something wrong? I'm trying to avoid using group by for performance reasons hence my interest to understand what's going on.
Thanks in advance
Yes, MySQL updates each record in a joined table at most once.
I cannot find it in the documentation, but practice says so.
I'll probably post it as a bug, so they at least add it to documentation:
CREATE TABLE updater (value INT NOT NULL);
INSERT
INTO updater
VALUES (1);
SELECT *
FROM updater;
value
---
1
UPDATE updater u
JOIN (
SELECT 1 AS newval
UNION ALL
SELECT 2
) q
SET u.value = u.value + newval;
SELECT *
FROM updater;
value
---
2
(expected 4).
SQL Server, by the way, behaves same in a multiple table UPDATE.
You can use:
UPDATE orders o
SET ordercount = ordercount +
(
SELECT COUNT(*)
FROM customers c
WHERE c.customerid = o.customerid
)
which is same on performance as long as you have an index on customers (customer_id)