SQL Query to identify accommodation booked - sql

I need to know what SQL statement to use in an accommodation database looking for a trip user

The error message is clear enough: the group by clause needs to be consistent with the select. Some databases are smart enough to understand that the name of the customer is functionally dependent on its id, and do not require that you put the name in the group by - but not SQL Server.
Also, you need to count on something that comes from the left joined table if you want 0 for customers without bookings.
Consider:
select c.customer_id, c.customer_name, count(ab.customer_id) as [number of accomm slots]
from customers c
left join accommodation_bookings ab on c.customer_id = ab.customer_id
group by c.customer_id, c.customer_name
I would take one step forward and pre-aggregate in a subquery. This is usually more efficient:
select c.customer_id, c.customer_name, coalesce(ab.cnt, 0) [number of accomm slots]
from customers c
left join (
select customer_id, count(*) cnt
from accommodation_bookings
group by customer_id
) ab on c.customer_id = ab.customer_id
You could also express this with a correlated subquery, or a lateral join:
select c.customer_id, c.customer_name, ab.*
from customers c
outer apply (
select count(*) [number of accomm slots]
from accommodation_bookings ab
where c.customer_id = ab.customer_id
) ab
This would take advantage of an index on accommodation_bookings(customer_id) (which should already be there if you have set up a foreign key).
Note: don't use single quotes for identifiers - they are meant for literal strings. In SQL Server, use the square brackets instead.

Related

SELECT "NOT IN", INNER JOIN and COUNT in SQL Query

I am trying to select which co-ordinates from OA table are NOT found in the CUSTOMER address table.
SELECT DISTINCT
OA.CO_ORDS
FROM
CUSTOMER
INNER JOIN
OA ON customer.address=oa.co_ords
ORDER BY ID ASC;
Returns the co-ordinates which ARE in the customer table. How do I return those that are not in the customer table?
Am I also able to COUNT how many of customers are is in each co-ordinate (The co-ords are not specific and not accurate, this is purely for query testing only)
SELECT DISTINCT
OA.CO_ORDS
FROM
CUSTOMER
INNER JOIN
OA ON customer.address=oa.co_ords
ORDER BY ID ASC;
We can use NOT EXISTS to find those co-ordinates which don't appear in the customer table:
SELECT co_ords
FROM oa
WHERE
NOT EXISTS
(SELECT 1 FROM customers
WHERE address = oa.co_ords)
ORDER BY id;
In order to count how many customers belong to a certain co-ordinate, we can use COUNT with GROUP BY, something like this:
SELECT c.address, COUNT(*)
FROM customers c
JOIN oa
ON c.address = oa.co_ords
GROUP BY c.address;
It could be better to count a specific column instead of *.
It could also be better to use an IN clause instead of JOIN the tables:
SELECT c.address, COUNT(*)
FROM customers c
WHERE address IN
(SELECT co_ords FROM oa)
GROUP BY c.address;
Such details depend on your exact table structure, you should please try this out or provide more details.
You could also do:
SELECT co_ords
FROM oa
MINUS
SELECT address
FROM customers;
which can sometimes be faster than doing an anti-join. Note that MINUS does a distinct on the resultset.

Join with a groupby operation in SQL

I am playing with W3Schools SQL environment. A pre-defined database is setup here.
Tables to be used: Customer and Orders.
To get all the info from Customer we can do:
SELECT * FROM [Customers]
To get Customers who have only less than 3 orders we do:
SELECT CustomerID, count(*) as num_orders FROM [Orders] group by customerID having num_orders<3
To get the Customers we have in London, we do:
SELECT * FROM [Customers] where city="London"
Question: How can I get, for every customer in London (with less than 3 orders), how many orders they have?
I know it has to be a Left join, as I want to keep all customers even if they have N/A orders (so, no records in "Orders"), but I am having a hard time to make it work.
I tried:
SELECT * FROM [Customers] where city="London"
left join (SELECT CustomerID, count(*) as num_orders FROM [Orders] group by customerID having num_orders<3) as data
on customers.CustomerID= data.CustomerID
But the environment gives no meaninful info about the error.
The proper syntax is:
SELECT c.*, o.num_orders
FROM [Customers] c LEFT JOIN
(SELECT o.CustomerID, COUNT(*) as num_orders
FROM [Orders] o
GROUP BY o.customerID
) o
ON c.CustomerID = o.CustomerID
WHERE c.city = 'London';
Notes:
The most important difference is the order of the clauses. WHERE comes after the FROM clause.
The HAVING clause is removed, because the question is for all customers in London.
Single quotes are used to quote London. Single quotes are the standard string delimiter.
The query uses table aliases, and these are specifically chosen to be very short and related to the table names.
All columns are qualified.

Use result of multiple rows to do arithmetic operation

I'm writing a query to multiply the count that I receive from subquery to fees amount, But I don't know how to do that. Any help/suggestion?
Oracle query is:
select courseid,coursename,fees*tmp
from course c join registration r on
r.courseid=c.courseid
and tmp IN (select count(*)
from course c join registration r on
r.courseid=c.courseid group by coursename);
I tried to use like a variable tmp ,But i don't think it works in oracle query. Is there an alternative way to do so?
You can't do that, because you can only select data from tables that appeared between FROM and WHERE. The IN operator is a quick way to save having to write a bunch of OR statements, it is not something that can establish a variable in the outer query.
Instead do something like:
select courseid,coursename,fees * COUNT(r.courseID) OVER(PARTITION BY c.coursename)
from course c join registration r on
r.courseid=c.courseid
Edit/update: you noted that this query produces too many rows and you only want to see distinct course names. In that case it would be better to just use the registrations table to count the number of people on the course and then multiply the fees:
SELECT
c.courseid, c.coursename, c.fees * COALESCE(r.numberOfstudents, 0) as courseWorth
FROM
course c
LEFT OUTER JOIN
(select courseid, COUNT(*) as numberofstudents FROM registration GROUP BY courseid) r
ON c.courseID = r.courseid
You can use a windowing function like Caius or you can use a join like this:
select courseid,coursename, fees * COALESCE(sub.cnt,0)
from course c
join registration r on r.courseid=c.courseid
left join (
select coursename, count(*) as cnt
from course c2
join registration r2 on r2.courseid=c2.courseid
group by coursename
) as sub;
note: I make no claim your joins are correct -- I'm basing this query off of your example not on any knowledge of your data model.

SQL Get aggregate as 0 for non existing row using inner joins

I am using SQL Server to query these three tables that look like (there are some extra columns but not that relevant):
Customers -> Id, Name
Addresses -> Id, Street, StreetNo, CustomerId
Sales -> AddressId, Week, Total
And I would like to get the total sales per week and customer (showing at the same time the address details). I have come up with this query
SELECT a.Name, b.Street, b.StreetNo, c.Week, SUM (c.Total) as Total
FROM Customers a
INNER JOIN Addresses b ON a.Id = b.CustomerId
INNER JOIN Sales c ON b.Id = c.AddressId
GROUP BY a.Name, c.Week, b.Street, b.StreetNo
and even if my SQL skill are close to none it looks like it's doing its job. But now I would like to be able to show 0 whenever the one customer don't have sales for a particular week (weeks are just integers). And I wonder if somehow I should get distinct values of the weeks in the Sales table, and then loop through them (not sure how)
Any help?
Thanks
Use CROSS JOIN to generate the rows for all customers and weeks. Then use LEFT JOIN to bring in the data that is available:
SELECT c.Name, a.Street, a.StreetNo, w.Week,
COALESCE(SUM(s.Total), 0) as Total
FROM Customers c CROSS JOIN
(SELECT DISTINCT s.Week FROM sales s) w LEFT JOIN
Addresses a
ON c.CustomerId = a.CustomerId LEFT JOIN
Sales s
ON s.week = w.week AND s.AddressId = a.AddressId
GROUP BY c.Name, a.Street, a.StreetNo, w.Week;
Using table aliases is good, but the aliases should be abbreviations for the table names. So, a for Addresses not Customers.
You should generate a week numbers, rather than using DISTINCT. This is better in terms of performance and reliability. Then use a LEFT JOIN on the Sales table instead of an INNER JOIN:
SELECT a.Name
,b.Street
,b.StreetNo
,weeks.[Week]
,COALESCE(SUM(c.Total),0) as Total
FROM Customers a
INNER JOIN Addresses b ON a.Id = b.CustomerId
CROSS JOIN (
-- Generate a sequence of 52 integers (13 x 4)
SELECT ROW_NUMBER() OVER (ORDER BY a.x) AS [Week]
FROM (VALUES(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) a(x)
CROSS JOIN (SELECT x FROM (VALUES(1),(1),(1),(1)) b(x)) b
) weeks
LEFT JOIN Sales c ON b.Id = c.AddressId AND c.[Week] = weeek.[Week]
GROUP BY a.Name
,b.Street
,b.StreetNo
,weeks.[Week]
Please try the following...
SELECT Name,
Street,
StreetNo,
Week,
SUM( CASE
WHEN Total IS NULL THEN
0
ELSE
Total
END ) AS Total
FROM Customers a
JOIN Addresses b ON a.Id = b.CustomerId
RIGHT JOIN Sales c ON b.Id = c.AddressId
GROUP BY a.Name,
c.Week,
b.Street,
b.StreetNo;
I have modified your statement in three places. The first is I changed your join to Sales to a RIGHT JOIN. This will join as it would with an INNER JOIN, but it will also keep the records from the table on the right side of the JOIN that do not have a matching record or group of records on the left, placing NULL values in the resulting dataset's fields that would have come from the left of the JOIN. A LEFT JOIN works in the same way, but with any extra records in the table on the left being retained.
I have removed the word INNER from your surviving INNER JOIN. Where JOIN is not preceded by a join type, an INNER JOIN is performed. Both JOIN and INNER JOIN are considered correct, but the prevailing protocol seems to be to leave the INNER out, where the RDBMS allows it to be left out (which SQL-Server does). Which you go with is still entirely up to you - I have left it out here for illustrative purposes.
The third change is that I have added a CASE statement that tests to see if the Total field contains a NULL value, which it will if there were no sales for that Customer for that Week. If it does then SUM() would return a NULL, so the CASE statement returns a 0 instead. If Total does not contain a NULL value, then the SUM() of all values of Total for that grouping is performed.
Please note that I am assuming that Total will not have any NULL values other than from the RIGHT JOIN. Please advise me if this assumption is incorrect.
Please also note that I have assumed that either there will be no missing Weeks for a Customer in the Sales table or that you are not interested in listing them if there are. Again, please advise me if this assumption is incorrect.
If you have any questions or comments, then please feel free to post a Comment accordingly.

How to reference a custom field in SQL

I am using mssql and am having trouble using a subquery. The real query is quite complicated, but it has the same structure as this:
select
customerName,
customerId,
(
select count(*)
from Purchases
where Purchases.customerId=customerData.customerId
) as numberTransactions
from customerData
And what I want to do is order the table by the number of transactions, but when I use
order by numberTransactions
It tells me there is no such field. Is it possible to do this? Should I be using some sort of special keyword, such as this, or self?
use the field number, in this case:
order by 3
Sometimes you have to wrestle with SQL's syntax (expected scope of clauses)
SELECT *
FROM
(
select
customerName,
customerId,
(
select count(*)
from Purchases
where Purchases.customerId=customerData.customerId
) as numberTransactions
from customerData
) as sub
order by sub.numberTransactions
Also, a solution using JOIN is correct. Look at the query plan, SQL Server should give identical plans for both solutions.
Do an inner join. It's much easier and more readable.
select
customerName,
customerID,
count(*) as numberTransactions
from
customerdata c inner join purchases p on c.customerID = p.customerID
group by customerName,customerID
order by numberTransactions
EDIT: Hey Nathan,
You realize you can inner join this whole table as a sub right?
Select T.*, T2.*
From T inner join
(select
customerName,
customerID,
count(*) as numberTransactions
from
customerdata c inner join purchases p on c.customerID = p.customerID
group by customerName,customerID
) T2 on T.CustomerID = T2.CustomerID
order by T2.numberTransactions
Or if that's no good you can construct your queries using temporary tables (#T1 etc)
There are better ways to get your result but just from your example query this will work on SQL2000 or better.
If you wrap your alias in single ticks 'numberTransactions' and then call ORDER BY 'numberTransactions'
select
customerName,
customerId,
(
select count(*)
from Purchases
where Purchases.customerId=customerData.customerId
) as 'numberTransactions'
from customerData
ORDER BY 'numberTransactions'
The same thing could be achieved by using GROUP BY and a JOIN, and you'll be rid of the subquery. This might be faster too.
I think you can do this in SQL2005, but not SQL2000.
You need to duplicate your logic. SQL Server isn't very smart at columns that you've named but aren't part of the dataset in your FROM statement.
So use
select
customerName,
customerId,
(
select count(*)
from Purchases p
where p.customerId = c.customerId
) as numberTransactions
from customerData c
order by (select count(*) from purchases p where p.customerID = c.customerid)
Also, use aliases, they make your code easier to read and maintain. ;)