Mysql query to get customer count from table - mysql

I have the following table
customer_id
id
product_type
serial_number
parent_prod_id
123
200
Camera
3222333
200
123
201
InstaCam
3322322
200
123
202
InstaCam
4332233
200
125
200
Camera
3222333
200
126
200
Camera
3222333
200
My query should return the customer count for each product type but if the same customer purchased a product such as InstaCam which is tied to the parent prod id Camera, then the customer count for the product InstaCam must be 0. In the above table, Camera was purchased by three different customers with customer ids 123, 125 and 126. Since InstaCam was also purchased by one of the customers who purchased the Camera and because the parent_prod_id of InstaCam is the same as the id of Camera, the same customer should not be counted again for the Instacam product so the customer count would be 0.
Expected output:
serial_number
product_type
customer_count
3222333
Camera
3
3322322
InstaCam
0
4332233
InstaCam
0
I have tried many solutions for hours with no luck. Any help would be greatly appreciated. Thank you.

This must work. Basically what this query does is sum the cases valid for your requirements.
These cases are:
The product is a parent
The product is a child but there is not a buy for the parent
Else => 0 (not sum)
Then, with this clasification, you can add the occurrences.
select d.serial_number, d.product_type, sum(counter) as customer_count
from (
select *,
case
when y.id = y.parent_prod_id then 1
when not exists (
select 1
from your_data yy
where y.customer_id=yy.customer_id
and yy.id = y.parent_prod_id
) then 1
else 0
end counter
from your_data y
) d
group by d.serial_number, d.product_type
You can test on this <>db_fiddle

You can do it with simple join and conditional aggregation.
Schema and insert statements:
create table yourtable(customer_id int, id int, product_type varchar(50), serial_number int, parent_prod_id int);
insert into yourtable values(123,200, 'Camera', 3222333,200);
insert into yourtable values(123,201, 'InstaCam', 3322322,200);
insert into yourtable values(123,202, 'InstaCam', 4332233,200);
insert into yourtable values(125,200, 'Camera', 3222333,200);
insert into yourtable values(126,200, 'Camera', 3222333,200);
Query:
select a.serial_number, a.product_type,sum(case when a.id=b.id then 1 else 0 end)customer_count
from yourtable a
left join yourtable b on a.parent_prod_id=b.id and a.customer_id=b.customer_id
group by a.serial_number, a.product_type
Output:
serial_number
product_type
customer_count
3222333
Camera
3
3322322
InstaCam
0
4332233
InstaCam
0
db<>fiddle here

To solve this, you will need to join the table to itself and compare sales.
First let's make the table and populate it with the supplied data:
DROP TABLE IF EXISTS `Sales`;
CREATE TABLE IF NOT EXISTS `Sales` (
`customer_id` int(11) UNSIGNED NOT NULL ,
`id` int(11) UNSIGNED NOT NULL ,
`product_type` varchar(80) NOT NULL DEFAULT '',
`serial_number` varchar(40) NOT NULL DEFAULT '',
`parent_prod_id` int(11) UNSIGNED NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO `Sales` (`customer_id`, `id`, `product_type`, `serial_number`, `parent_prod_id`)
VALUES (123, 200, 'Camera', '3222333', 200),
(123, 201, 'InstaCam', '3322322', 200),
(123, 202, 'InstaCam', '4332233', 200),
(125, 200, 'Camera', '3222333', 200),
(126, 200, 'Camera', '3222333', 200);
To get the results you seek, we can use a query like this:
SELECT s.`serial_number`, s.`product_type`,
COUNT(DISTINCT CASE WHEN pp.`id` IS NOT NULL THEN NULL ELSE s.`customer_id` END) as `customer_count`
FROM `Sales` s LEFT OUTER JOIN `Sales` pp ON s.`customer_id` = pp.`customer_id`
AND s.`parent_prod_id` = pp.`id`
AND s.`id` <> pp.`id`
GROUP BY s.`serial_number`, s.`product_type`;
This will give you a result like this:
serial_number
product_type
customer_count
3222333
Camera
3
3322322
InstaCam
0
4332233
InstaCam
0
Now to test this, let's add a record for a customer who bought only an InstaCam:
INSERT INTO `Sales` (`customer_id`, `id`, `product_type`, `serial_number`, `parent_prod_id`)
VALUES (131, 201, 'InstaCam', '3322322', 200);
Run the same query as before, and you'll get this:
serial_number
product_type
customer_count
3222333
Camera
3
3322322
InstaCam
1
4332233
InstaCam
0
Next time I answer a question, I'll make sure I have a cup of coffee first 🤪

You can use distinct on customer_id such as SELECT count(distinct customer_id) so to count a customer only once.
https://www.mysqltutorial.org/mysql-distinct.aspx

Related

How to fetch the latest status from a table of Updates and join it with table of details in MySQL?

The two tables that I am using are:
CREATE TABLE `UPDATE_TABLE` (
`DETAIL_ID` VARCHAR(10),
`UPDATE_ID` VARCHAR(10) NOT NULL,
`STATUS` ENUM('COMPLETE','INCOMPLETE') NOT NULL,
`UPDATED_ON` DATETIME NOT NULL,
PRIMARY KEY (`UPDATE_ID`),
FOREIGN KEY(`DETAIL_ID`) REFERENCES `TASK_DETAIL` (`DETAIL_ID`)
);
CREATE TABLE `DETAIL_TABLE` (
`DETAIL_ID` VARCHAR(10) NOT NULL,
`NAME` VARCHAR(50),
);
I need the details: DETAIL_ID, NAME, STATUS (Latest), UPDATED_ON in my final result.
What I am currently doing is:
SELECT t.* FROM update_table t WHERE UPDATED_ON in (
SELECT MAX(UPDATED_ON) as latest_update FROM update_table group by DETAIL_ID
) to fetch the details of the latest updates, and do a left join on detail_table.
But the problem comes when multiple detail_ids have the same updated_on.
For Eg:
UPDATE_TABLE:
100, 1, INCOMPLETE 2021-04-03 17:03:34
200, 2, INCOMPLETE 2021-04-03 17:03:34
300, 3, INCOMPLETE 2021-04-03 17:03:34
200, 4, COMPLETE 2021-04-04 11:50:41
So this gives me 2 updates of detail_id = 200.
I need only the latest update.
A SQLAlchemy Query would be a great bonus!
If UPDATED_ON field on UPDATE_TABLE never has same value for 1 DETAIL_ID then this should be work for your case
CREATE TABLE `UPDATE_TABLE` (
`DETAIL_ID` VARCHAR(10),
`UPDATE_ID` VARCHAR(10) NOT NULL,
`STATUS` ENUM('COMPLETE','INCOMPLETE') NOT NULL,
`UPDATED_ON` DATETIME NOT NULL,
PRIMARY KEY (`UPDATE_ID`)
);
Insert into `UPDATE_TABLE` values('100','1','INCOMPLETE','2021-04-03 17:03:34');
Insert into `UPDATE_TABLE` values('200','2','INCOMPLETE','2021-04-03 17:03:34');
Insert into `UPDATE_TABLE` values('300','3','INCOMPLETE','2021-04-03 17:03:34');
Insert into `UPDATE_TABLE` values('200','4','COMPLETE','2021-04-04 11:50:41');
DETAIL_ID
UPDATE_ID
STATUS
UPDATED_ON
100
1
INCOMPLETE
2021-04-03 17:03:34
200
2
INCOMPLETE
2021-04-03 17:03:34
300
3
INCOMPLETE
2021-04-03 17:03:34
200
4
COMPLETE
2021-04-04 11:50:41
Here is what you need:
select ut.*
from `UPDATE_TABLE` ut
inner join (select detail_id, max(updated_on) as 'updated_on'
from `UPDATE_TABLE`
group by detail_id) tbl
on ut.detail_id = tbl.detail_id and ut.updated_on = tbl.updated_on
order by detail_id
result:
DETAIL_ID
UPDATE_ID
STATUS
UPDATED_ON
100
1
INCOMPLETE
2021-04-03 17:03:34
200
4
COMPLETE
2021-04-04 11:50:41
300
3
INCOMPLETE
2021-04-03 17:03:34
dbfiddle here
You can try the below SQL query to get the results you need:
SELECT distinct(ut.DETAIL_ID), dt.NAME, ut.STATUS, ut.UPDATED_ON
FROM UPDATE_TABLE ut, DETAIL_TABLE dt
WHERE ut.DETAIL_ID = dt.DETAIL_ID
AND ut.UPDATED_ON = (SELECT MAX(UPDATED_ON) FROM UPDATE_TABLE WHERE DETAIL_ID = ut.DETAIL_ID);

Using MIN value after JOIN MAX(date) MYSQL

I have 3 table. manufacturers, products and prices
I want to get the last price of product and select min price of them.
Table manufacturers:
# manufacturers
id name
1 Manufacturer 1
2 Manufacturer 2
Table products:
# products
id name
1 Product 1
2 Product 2
Table prices:
# prices
id price manufacturerId createdAt
1 10 1 '2019-09-09 00:00:00'
2 20 1 '2019-09-10 00:00:00'
3 11 2 '2019-09-09 00:00:00'
4 21 2 '2019-09-10 00:00:00'
Full code:
DROP DATABASE if exists ssg ;
CREATE DATABASE ssg;
USE ssg;
# Create database manufacturers
CREATE TABLE manufacturers (id INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(256) NOT NULL);
# Insert value
INSERT INTO manufacturers (name) VALUES ('Manufacturer 1');
INSERT INTO manufacturers (name) VALUES ('Manufacturer 2');
# Create database products
CREATE TABLE products (id INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(256) NOT NULL);
# Insert value
INSERT INTO products (name) VALUES ('Product 1');
# Create database prices
CREATE TABLE prices (id INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
productId INT(11) UNSIGNED NOT NULL,
price BIGINT UNSIGNED NOT NULL,
manufacturerId INT(11) UNSIGNED NOT NULL,
createdAt DATETIME NOT NULL);
# Insert value
INSERT INTO prices (productId, price, manufacturerId, createdAt) VALUES (1, 10, 1, '2019-09-09 00:00:00');
INSERT INTO prices (productId, price, manufacturerId, createdAt) VALUES (1, 20, 1, '2019-09-10 00:00:00');
INSERT INTO prices (productId, price, manufacturerId, createdAt)VALUES (1, 11, 2, '2019-09-09 00:00:00');
INSERT INTO prices (productId, price, manufacturerId, createdAt)VALUES (1, 21, 2, '2019-09-10 00:00:00');
# Query
SELECT products.id, products.name, lastValue.price as latestPrice, lastValue.manufacturerId
FROM products
LEFT JOIN(
SELECT productId, COUNT(DISTINCT manufacturerId) AS total
FROM prices
GROUP BY prices.productId) counts ON counts.productId = products.id
LEFT JOIN (
SELECT prices.*
FROM (
SELECT productId, MAX(createdAt) createdAt
FROM prices
GROUP BY productId) latest
JOIN prices ON latest.productId = prices.productId
AND prices.createdAt = latest.createdAt
) lastValue
ON lastValue.productId = products.id
and I got:
id name latestPrice manufacturerId
1 Product 1 20 1
1 Product 1 21 2
So how can I receive products with only with the MIN of latestPrice.
I have to post it in http://sqlfiddle.com/#!9/418cb7/1 . Please "Build Schema" then "Run SQL"
Sorry for my bad english.
In MySQL 8.0, you can do this with window functions only:
select id, name, price, manufacturerId
from (
select
t.*,
rank() over(order by price) rn2
from (
select
p.id,
p.name,
i.price,
i.manufacturerId,
rank() over(partition by p.id order by i.createdAt desc) rn1
from products p
inner join prices i on i.productId = p.id
) t
where rn1 = 1
) t
where rn2 = 1
This phrases as:
first rank the prices of each product by descending date, and filter on the latest price per product
then rank the all the latest prices by ascending price, and filter on the lowest of them
Demo on DB Fiddle:
id | name | price | manufacturerId
-: | :-------- | ----: | -------------:
1 | Product 1 | 20 | 1

SQL Server 2008 - excluding from number of ranges of numbers

I need help with a SQL script.
I have an orders table with a card_number column and I have another table called card_range which stores card_number ranges per client with a column to flag whether it's active or not active.
When I select data from the orders table I would like to exclude all card_number ranges which are inactive
Eg. orders table
order_id card_number
1 101
2 102
3 201
4 301
card_range table, active 0 = no, 1 = yes
start_card_number end_card_number active
101 199 0
201 299 1
301 399 0
So the only data I want to return from the orders table is
order_id card_number
3 201
The script that I'm trying to figure out is the looping thru the card_range table...thanks
Assuming that card ranges don't overlap:
declare #orders as table ( order_id int, card_number int )
insert into #orders ( order_id, card_number ) values
( 1, 101 ), ( 2, 102 ), ( 3, 201 ), ( 4, 301 )
declare #card_range as table ( start_card_number int, end_card_number int, active bit )
insert into #card_range ( start_card_number, end_card_number, active ) values
( 101, 199, 0 ), ( 201, 299, 1 ), ( 301, 399, 0 )
select order_id, card_number
from #orders as o inner join
#card_range as cr on
cr.start_card_number <= o.card_number and o.card_number <= cr.end_card_number and
cr.active = 1

How to find if a list/set is contained within another list

I have a list of product IDs and I want to find out which orders contain all those products. Orders table is structured like this:
order_id | product_id
----------------------
1 | 222
1 | 555
2 | 333
Obviously I can do it with some looping in PHP but I was wondering if there is an elegant way to do it purely in mysql.
My ideal fantasy query would be something like:
SELECT order_id
FROM orders
WHERE (222,555) IN GROUP_CONCAT(product_id)
GROUP BY order_id
Is there any hope or should I go read Tolkien? :) Also, out of curiosity, if not possible in mysql, is there any other database that has this functionality?
You were close
SELECT order_id
FROM orders
WHERE product_id in (222,555)
GROUP BY order_id
HAVING COUNT(DISTINCT product_id) = 2
Regarding your "out of curiosity" question in relational algebra this is achieved simply with division. AFAIK no RDBMS has implemented any extension that makes this as simple in SQL.
I have a preference for doing set comparisons only in the having clause:
select order_id
from orders
group by order_id
having sum(case when product_id = 222 then 1 else 0 end) > 0 and
sum(case when product_id = 555 then 1 else 0 end) > 0
What this is saying is: get me all orders where the order has at least one product 222 and at least one product 555.
I prefer this for two reasons. The first is generalizability. You can arrange more complicated conditions, such as 222 or 555 (just by changing the "and" to and "or"). Or, 333 and 555 or 222 without 555.
Second, when you create the query, you only have to put the condition in one place, in the having clause.
Assuming your database is properly normalized, i.e. there's no duplicate Product on a given Order
Mysqlism:
select order_id
from orders
group by order_id
having sum(product_id in (222,555)) = 2
Standard SQL:
select order_id
from orders
group by order_id
having sum(case when product_id in (222,555) then 1 end) = 2
If it has duplicates:
CREATE TABLE tbl
(`order_id` int, `product_id` int)
;
INSERT INTO tbl
(`order_id`, `product_id`)
VALUES
(1, 222),
(1, 555),
(2, 333),
(1, 555)
;
Do this then:
select order_id
from tbl
group by order_id
having count(distinct case when product_id in (222,555) then product_id end) = 2
Live test: http://www.sqlfiddle.com/#!2/fa1ad/5
CREATE TABLE orders
( order_id INTEGER NOT NULL
, product_id INTEGER NOT NULL
);
INSERT INTO orders(order_id,product_id) VALUES
(1, 222 ) , (1, 555 ) , (2, 333 )
, (3, 222 ) , (3, 555 ) , (3, 333 ); -- order#3 has all the products
CREATE TABLE products AS (SELECT DISTINCT product_id FROM orders);
SELECT *
FROM orders o1
--
-- There should not exist a product
-- that is not part of our order.
--
WHERE NOT EXISTS (
SELECT *
FROM products pr
WHERE 1=1
-- extra clause: only want producs from a literal list
AND pr.product_id IN (222,555,333)
-- ... that is not part of our order...
AND NOT EXISTS ( SELECT *
FROM orders o2
WHERE o2.product_id = pr.product_id
AND o2.order_id = o1.order_id
)
);
Result:
order_id | product_id
----------+------------
3 | 222
3 | 555
3 | 333
(3 rows)

mysql many-to-one and group by giving unusual results

im having difficulty with the following fairly simple setup:
CREATE TABLE IF NOT EXISTS invoices (
id int(11) NOT NULL auto_increment,
PRIMARY KEY (id)
);
CREATE TABLE IF NOT EXISTS invoices_items (
id int(11) NOT NULL auto_increment,
invoice_id int(11) NOT NULL,
description text NOT NULL,
amount decimal(10,2) NOT NULL default '0.00',
PRIMARY KEY (id)
);
CREATE TABLE IF NOT EXISTS invoices_payments (
id int(11) NOT NULL auto_increment,
invoice_id int(11) NOT NULL,
amount decimal(10,2) NOT NULL default '0.00',
PRIMARY KEY (id)
);
some data:
INSERT INTO invoices (id) VALUES (1);
INSERT INTO invoices_items (id, invoice_id, description, amount) VALUES
(1, 1, 'Item 1', '750.00'),
(2, 1, 'Item 2', '750.00'),
(3, 1, 'Item 3', '50.00'),
(4, 1, 'Item 4', '150.00');
INSERT INTO invoices_payments (id, invoice_id, amount) VALUES
(1, 1, '50.00'),
(2, 1, '1650.00');
and the sql yielding unusual results:
select invoices.id,
ifnull(sum(invoices_payments.amount),0) as payments_total,
ifnull(count(invoices_items.id),0) as item_count
from invoices
left join invoices_items on invoices_items.invoice_id=invoices.id
left join invoices_payments on invoices_payments.invoice_id=invoices.id
group by invoices.id
results in the (erroneous) output
id payments_total item_count
1 6800.00 8
now, as evidenced by there being infact only four 'invoice_item' rows, i dont understand why mysql is not grouping properly.
EDIT
i know i can do something like this:
select x.*, ifnull(sum(invoices_payments.amount),0) as payments_total from (
select invoices.id,
ifnull(count(invoices_items.id),0) as item_count
from invoices
left join invoices_items on invoices_items.invoice_id=invoices.id
group by invoices.id
) as x left join invoices_payments on invoices_payments.invoice_id=x.id
group by x.id
but i want to know if im doing something wrong in the first query - i cant immediately see why the first query is giving incorrect results! :(
Your join logic is incorrect. In your join, you specify invoices_items.invoice_id = invoices.id. You also specify invoices_payments.invoice_id = invoices.id. Because of transitivity, you end up with:
invoices_items.invoice_id = invoices.id
invoices_payments.invoice_id = invoices.id
invoice_items.invoice_id = invoices_payments.invoice_id
The sum of the 2 invoice payments is $1700. For every invoice payment, there are 4 invoice_items that satisfy the above relations. $1700 * 4 = $6800.
For every invoice item, there will be two invoice payments that satisfy the above relations. 4 invoice items * 2 = 8 count.
There are two tables with a many:one relationship with invoices. Your count is the cartesian product.
The payments should be applied to the invoice, not the invoice items. Get the invoice total first, then join the payments to it.
This may be similar to what you are looking for:
SELECT
invoice_total.invoice_id,
invoice_total.amount as invoice_amount,
payments_total.amount as total_paid
FROM
(
SELECT
invoice_id,
SUM(amount) as amount
FROM
invoices_items
GROUP BY
invoice_id
) invoice_total
INNER JOIN
(
SELECT
invoice_id,
SUM(amount) as amount
FROM
invoices_payments
GROUP BY
invoice_id
) payments_total
ON invoice_total.invoice_id = payments_total.invoice_id;
edit:
ah, sorry - see your point now. The reason you're getting unexpected results is that this query:
SELECT *
FROM invoices
LEFT JOIN invoices_items ON invoices_items.invoice_id = invoices.id
LEFT JOIN invoices_payments ON invoices_payments.invoice_id = invoices.id;
results in this:
id id invoice_id description amount id invoice_id amount
1 1 1 Item 1 750.00 1 1 50.00
1 1 1 Item 1 750.00 2 1 1650.00
1 2 1 Item 2 750.00 1 1 50.00
1 2 1 Item 2 750.00 2 1 1650.00
1 3 1 Item 3 50.00 1 1 50.00
1 3 1 Item 3 50.00 2 1 1650.00
1 4 1 Item 4 150.00 1 1 50.00
1 4 1 Item 4 150.00 2 1 1650.00
As you can see you get every invoices_items record once each for every invoices_payments record. You're going to have to grab (i.e. group) them separately.
Note that the GROUP BY clause in your initial query is redundant.
Here's what you need:
SELECT
invoices.id,
payments_total.payments_total,
IFNULL(COUNT(invoices_items.id),0) AS item_count
FROM invoices
LEFT JOIN invoices_items ON invoices.id = invoices_items.invoice_id
LEFT JOIN (
SELECT invoice_id,
IFNULL(SUM(invoices_payments.amount),0) AS payments_total
FROM invoices_payments
GROUP BY invoice_id
) AS payments_total ON invoices.id = payments_total.invoice_id
;