How to count multiple columns grouping by rows in MySQL? - mysql

I have two tables, "keywords" and "stats" and want to know per keyword how many results each merchant has. So one row per keyword.
Desired result e.g.:
KWD | RESULTS Amazon | RESULTS eBay
test 3 5
second 6 2
The tables:
create table keywords
(
ID mediumint unsigned auto_increment
primary key,
KEYWORD varchar(255) null
);
create table stats
(
MERCHANT_ID tinyint unsigned not null,
TYPE_ID mediumint unsigned not null comment 'the ID of the coresponding type. E.g. kw_id from keywords',
RESULTS smallint unsigned null,
DATE date not null,
primary key (DATE, MERCHANT_ID, TYPE_ID)
)
comment 'How many results does each merchant have per search?';
Sample data:
-- keywords
insert into test.keywords (ID, KEYWORD) values (1, 'testing');
insert into test.keywords (ID, KEYWORD) values (2, 'blablub');
-- stats
insert into test.stats (MERCHANT_ID, TYPE_ID, RESULTS, DATE) values (1, 1, 33, '2021-07-06');
insert into test.stats (MERCHANT_ID, TYPE_ID, RESULTS, DATE) values (1, 2, 3, '2021-07-06');
insert into test.stats (MERCHANT_ID, TYPE_ID, RESULTS, DATE) values (2, 1, 22, '2021-07-06');
insert into test.stats (MERCHANT_ID, TYPE_ID, RESULTS, DATE) values (2, 2, 6, '2021-07-06');
The query:
select
kwd.KEYWORD,
mss.MERCHANT_ID,
mss.RESULTS
from keywords kwd
LEFT JOIN stats mss ON mss.TYPE_ID = kwd.ID
where
date = 20210705
group by kwd.ID
There are about 10 merchants. Is it possible to get one row per keyword and have the number of results per merchant in seperate colunns?

Try something like this:
select
kwd.KEYWORD,
SUM(IF(mss.MERCHANT_ID = 'amazon', mss.RESULTS, 0)) as `amazon_sum`,
SUM(IF(mss.MERCHANT_ID = 'eBay', mss.RESULTS, 0)) as `eBay_sum`
from keywords kwd
LEFT JOIN stats mss ON mss.TYPE_ID = kwd.ID
where
date = 20210705
group by kwd.ID

Related

MySQL select row from one table with multiple rows in a second table and get array of multi row in selected row

i have one table containing "Client" information, and another including "Tickets" information for each client.
int-------| varchar -------| varchar
client_id | client_name | client_tickets
----------+----------------+--------------
1 | Title one | 1,2
2 | Title two | 2,3
Simplified tickets table
int--------| varchar -------| varchar
ticket_id | ticket_name | ticket_price
-----------+-------------+--------------
1 | ticketone | 30
2 | tickettwo | 40
3 | ticketthree | 50
4 | ticketfour | 60
5 | ticketfive | 70
With the above two tables, I want to produce a single table with a single query with all the pertinent information to generate a search grid
So as to give the following output :
client_id | client_name | client_tickets | ticket_names | ticket_prices
----------+----------------+----------------+-----------------------+--
1 | Title one | 1,2 | ticketone,tickettwo | 30,40
2 | Title two | 2,3 | tickettwo,ticketthree | 40,50
ticket_names,ticket_ids,client_name are varchar
I want to receive the final 5 columns with one request
for example :
SELECT s.*,
(SELECT GROUP_CONCAT(ticket_name SEPARATOR ',') FROM tickets_table WHERE ticket_id IN(s.client_tickets)) AS ticket_names,
(SELECT GROUP_CONCAT(ticket_price SEPARATOR ',') FROM tickets_table WHERE ticket_id IN(s.client_tickets)) AS ticket_prices
FROM client_table s where s.client_id=1
Which seems to have a problem
Do you have a better suggestion?
Please make your suggestions
Update :
To clean the result I want
The following code has two querys,
I want this code to be done with a query
$client_result = $conn->query("SELECT * FROM client_table where client_id=1");
while($client_row = $client_result->fetch_assoc()) {
$ticket_result = $conn->query("SELECT * FROM tickets_table where ticket_id IN ($client_row['client_tickets'])");
while($ticket_row = ticket_result->fetch_assoc()) {
echo $ticket_row['ticket_name']."<br>";
}
}
update 2
i use suggest #raxi , but my mariadb is 10.4.17-MariaDB and don't support JSON_ARRAYAGG , for resolve it according to the reference Creating an aggregate function
, Using SQL
DELIMITER //
DROP FUNCTION IF EXISTS JSON_ARRAYAGG//
CREATE AGGREGATE FUNCTION IF NOT EXISTS JSON_ARRAYAGG(next_value TEXT) RETURNS TEXT
BEGIN
DECLARE json TEXT DEFAULT '[""]';
DECLARE CONTINUE HANDLER FOR NOT FOUND RETURN json_remove(json, '$[0]');
LOOP
FETCH GROUP NEXT ROW;
SET json = json_array_append(json, '$', next_value);
END LOOP;
END //
DELIMITER ;
What you want a fairly straightforward SELECT query with some LEFT/INNER JOIN(s).
This website has some good examples/explanations which seem very close to your need: https://www.mysqltutorial.org/mysql-inner-join.aspx
I would give you a quick working example, but it is not really clear to me what datatype the relevant columns are. Both tables' _id-columns are likely some variant of INTEGER, are they also both primary keys (or otherwise atleast indexed ?), the client_name/ticket_name are likely VARCHAR/TEXT/STRING types, but how exactly is the remaining column stored? as json or array or ? (+details)
Also you tagged your post with PHP, are you just after the SQL query ? or looking for PHP code with the SQL inside it.
updated
Improved version of the schema
CREATE TABLE clients (
client_id SERIAL,
client_name VARCHAR(255) NOT NULL,
PRIMARY KEY (client_id)
);
CREATE TABLE tickets (
ticket_id SERIAL,
ticket_name VARCHAR(255) NOT NULL,
ticket_price DECIMAL(10,2) NOT NULL,
PRIMARY KEY (ticket_id)
);
-- A junction table to glue those 2 tables together (N to N relationship)
CREATE TABLE client_tickets (
client_id BIGINT UNSIGNED NOT NULL,
ticket_id BIGINT UNSIGNED NOT NULL,
PRIMARY KEY (client_id, ticket_id)
);
I have changed the datatypes.
client_name and ticket_name are still VARCHARS. I've flagged them as NOT NULL (eg: required fields), but you can remove that part if you don't like that.
client_id/ticket_id/ticket_price are also NOT NULL but changing that has negative side-effects.
ticket_price is now a DECIMAL field, which can store numbers such as 1299.50 or 50.00 The (10,2) bit means it covers every possible number up to 8 whole digits (dollars/euros/whatever), and 2 decimals (cents). so you can store anything from $ -99.999.999,99 to $ 99.999.999,99 .
in SQL always write numbers (like lets say 70k) in this notation: 70000.00 (eg: a dot, not a comma; and no thousandseperators).
client_id and ticket_id are both SERIALs now, which is shorthand for BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE and theyre both PRIMARY KEYs on top of that. That probably sounds complicated but they're still just ordinary INTEGERs with values like 4 or 12 etc.
The UNIQUE bit prevents you from having 2 clients with the same ID number, and the AUTO_INCREMENT means that when you add a new client, you dont have to specify an ID (though you are allowed to); you can just do:
INSERT INTO clients (client_name) values ('Fantastic Mr Fox');
and the client_id will automatically be set (incrementing over time). And the same goes for ticket_id in the other table.
.
I've replaced your original client_tickets column, into a separate junction table.
Records in there store the client_id of a client and the ticket_id that belongs to them.
A client can have multiple records in the junction table (one record for each ticket they own).
Likewise, a ticket can be mentioned on any number of rows.
It's possible for a certain client_id to not have any records in the junction table.
Likewise, it's possible for a certain ticket_id to not have any records in the junction table.
Identical records cannot exist in this table (enforced by PRIMARY KEY).
Testdata
Next, we can put some data in there to be able to test it:
-- Create some tickets
INSERT INTO tickets (ticket_id, ticket_name, ticket_price) values (1, 'ticketone', '30' );
INSERT INTO tickets (ticket_id, ticket_name, ticket_price) values (2, 'tickettwo', '40' );
INSERT INTO tickets (ticket_id, ticket_name, ticket_price) values (3, 'ticketthree', '50' );
INSERT INTO tickets (ticket_id, ticket_name, ticket_price) values (4, 'ticketfour', '60' );
INSERT INTO tickets (ticket_id, ticket_name, ticket_price) values (5, 'ticketfive', '70' );
INSERT INTO tickets (ticket_id, ticket_name, ticket_price) values (6, 'ticketsix', '4' );
INSERT INTO tickets (ticket_id, ticket_name, ticket_price) values (7, 'ticketseven', '9' );
INSERT INTO tickets (ticket_id, ticket_name, ticket_price) values (8, 'ticketeight', '500' );
-- Create some users, and link them to some of these tickets
INSERT INTO clients (client_id, client_name) values (1, 'John');
INSERT INTO client_tickets (client_id, ticket_id) values (1, 3);
INSERT INTO client_tickets (client_id, ticket_id) values (1, 7);
INSERT INTO client_tickets (client_id, ticket_id) values (1, 1);
INSERT INTO clients (client_id, client_name) values (2, 'Peter');
INSERT INTO client_tickets (client_id, ticket_id) values (2, 5);
INSERT INTO client_tickets (client_id, ticket_id) values (2, 2);
INSERT INTO client_tickets (client_id, ticket_id) values (2, 3);
INSERT INTO clients (client_id, client_name) values (3, 'Eddie');
INSERT INTO client_tickets (client_id, ticket_id) values (3, 8);
INSERT INTO clients (client_id, client_name) values (9, 'Fred');
-- Note: ticket #3 is owned by both client #1/#2;
-- Note: ticket #4 and #6 are unused;
-- Note: client #9 (Fred) has no tickets;
Queries
Get all the existing relationships (ticket-less clients are left out & owner-less tickets are left out)
SELECT clients.*
, tickets.*
FROM client_tickets AS ct
INNER JOIN clients ON ct.client_id = clients.client_id
INNER JOIN tickets ON ct.ticket_id = tickets.ticket_id
ORDER BY clients.client_id ASC
, tickets.ticket_id ASC ;
Get all the tickets that are still free (owner-less)
SELECT tickets.*
FROM tickets
WHERE tickets.ticket_id NOT IN (
SELECT ct.ticket_id
FROM client_tickets AS ct
)
ORDER BY tickets.ticket_id ASC ;
Get a list of ALL clients (even ticketless ones), and include how many tickets each has and the total price of their tickets.
SELECT clients.*
, COALESCE(COUNT(tickets.ticket_id), 0) AS amount_of_tickets
, COALESCE(SUM(tickets.ticket_price), 0.00) AS total_price
FROM clients
LEFT JOIN client_tickets AS ct ON ct.client_id = clients.client_id
LEFT JOIN tickets ON ct.ticket_id = tickets.ticket_id
GROUP BY clients.client_id
ORDER BY clients.client_id ASC ;
Put all the juicy info together (owner-less tickets are left out)
SELECT clients.*
, COALESCE(COUNT(sub.ticket_id), 0) AS amount_of_tickets
, COALESCE(SUM(sub.ticket_price), 0.00) AS total_price
, JSON_ARRAYAGG(sub.js_tickets_row) AS js_tickets_rows
FROM clients
LEFT JOIN client_tickets AS ct ON ct.client_id = clients.client_id
LEFT JOIN (
SELECT tickets.*
, JSON_OBJECT( 'ticket_id', tickets.ticket_id
, 'ticket_name', tickets.ticket_name
, 'ticket_price', tickets.ticket_price
) AS js_tickets_row
FROM tickets
) AS sub ON ct.ticket_id = sub.ticket_id
GROUP BY clients.client_id
ORDER BY clients.client_id ASC ;
-- sidenote: output column `js_tickets_rows` (a json array) may contain NULL values
An list of all tickets with some aggregate data
SELECT tickets.*
, IF(COALESCE(COUNT(clients.client_id), 0) > 0
, TRUE, FALSE) AS active
, COALESCE( COUNT(clients.client_id), 0) AS amount_of_clients
, IF(COALESCE( COUNT(clients.client_id), 0) > 0
, GROUP_CONCAT(clients.client_name SEPARATOR ', ')
, NULL) AS client_names
FROM tickets
LEFT JOIN client_tickets AS ct ON ct.ticket_id = tickets.ticket_id
LEFT JOIN clients ON ct.client_id = clients.client_id
GROUP BY tickets.ticket_id
ORDER BY tickets.ticket_id ASC
, clients.client_id ASC ;

How to count all rows with multiple table joins and where conditions?

I have the following tables:
create table loans
(
id int null,
status int null,
user_id int null
);
INSERT INTO loans VALUES (1, 1, 1);
INSERT INTO loans VALUES (2, 0, 1);
INSERT INTO loans VALUES (3, 1, 1);
create table deals
(
id int null,
status int null,
user_id int null
);
INSERT INTO deals VALUES (2, 0, 1);
INSERT INTO deals VALUES (3, 0, 1);
create table listings
(
id int null,
status int null,
user_id int null
);
INSERT INTO listings VALUES (1, 1, 1);
INSERT INTO listings VALUES (2, 1, 1);
INSERT INTO listings VALUES (3, 1, 1);
And have the following SQL:
SELECT COUNT(*) AS active_items
FROM loans
LEFT JOIN deals ON deals.user_id = 1
LEFT JOIN listings ON listings.user_id = 1
WHERE
loans.status = 1
AND deals.status = 1
AND listings.status = 1
AND loans.user_id = 1
The goal is to count all the rows where each table item has a status of 1, leaving out any that have a status of 0. My query which I have made seems to only return 0 all the time and I do not understand why? How can I query the database so I can find each loan, deal and listing which has a status of 1 and returns in one total called active_items? Why does my query not work?
DB Fiddle: https://www.db-fiddle.com/f/g9CoA9CdDujqzG4ZpgmJXh/1
The output for active_items is expected to be 5.
Don't use JOIN for this, since you're not relating the tables to each other. Just do 3 separate queries and add the counts.
SELECT SUM(count) AS total
FROM (
SELECT COUNT(*) AS count
FROM loans
WHERE user_id = 1 AND status = 1
UNION ALL
SELECT COUNT(*) AS count
FROM deals
WHERE user_id = 1 AND status = 1
UNION ALL
SELECT COUNT(*)
FROM listings
WHERE user_id = 1 AND status = 1
) AS x
DEMO
It is not 100% clear to me what you are trying to check.
But if I understand it correctly, I think the problem you have, is you are only checking for the user_id = 1, which might not have status 1 in all the tables (I really can't be sure without seeing your data).
I think you want to do something like:
SELECT COUNT(*) AS active_items
FROM loans
INNER JOIN deals ON deals.user_id = loans.user_id
INNER JOIN listings ON listings.user_id = loans.user_id
WHERE
loans.status = 1
AND deals.status = 1
AND listings.status = 1

MySQL Select duplicates with LEAST condition

I'm trying to find duplicates and select the result with the least value combination in a table.
Until now I'm only able to select the result that has the lowest value on a column using MIN(). I thought it would be easy to just replace MIN with LEAST and change the columns.
Here's a layout:
CREATE TABLE `index`.`products` ( `id` INT NOT NULL AUTO_INCREMENT , `name` VARCHAR(10) NOT NULL , `price` INT NOT NULL , `availability` INT NOT NULL , PRIMARY KEY (`id`)) ENGINE = InnoDB;
INSERT INTO `products` (`id`, `name`, `price`, `availability`) VALUES
(NULL, 'teste', '10', '1'),
(NULL, 'teste', '5', '2'),
(NULL, 'teste', '3', '3');
The simplified layout
id - name - price - availabilty
1 - test - 10 - 1
2 - test - 5 - 2
3 - test - 3 - 3
using the following query:
select name, MIN(price) from products group by name having count(*) > 1
gets me the lowest price. I'm trying to get the lowest price and lowest availabilty.
select name, LEAST(price, availability) from products group by name having count(*) > 1
This doesn't work.
Clarification: I want to select the row with the lowest price and lowest availabity. In this case it should be the first one I guess.
I should clarifity that 1=available, 2=not available and 3=coming soon
The statement to select lowest price for the best availability is:
set sql_mode=only_full_group_by;
SELECT
name, MIN(price), availability
FROM
products
JOIN
(
SELECT
name, MIN(availability) availability
FROM
products
GROUP BY name
) as x
USING (name , availability)
GROUP BY name , availability;

Can I get GROUP_CONCAT with COUNT() on multiple columns from the same table?

I have the following table:
CREATE TABLE entries(
`id` INT UNSIGNED AUTO_INCREMENT,
`level` INT UNSIGNED,
`type` CHAR(2),
`attribute` INT UNSIGNED,
PRIMARY KEY(id)
);
From this table, I'm currently doing the same query for 3 different columns:
SELECT level, COUNT(*) FROM entries GROUP BY level;
SELECT type, COUNT(*) FROM entries GROUP BY type;
SELECT attribute, COUNT(*) FROM entries GROUP BY attribute;
I know I can use GROUP_CONCAT to get the DISTINCT entries for each of these in a single SQL call:
SELECT GROUP_CONCAT(DISTINCT level) AS levels, GROUP_CONCAT(DISTINCT type) AS types, GROUP_CONCAT(attribute) AS attributes FROM entries;
But can I manipulate this query to include the counts? OR is there a different way that I can get the distinct values and counts for these columns in a single call?
EDIT: here's some data to add to the table
INSERT INTO entries (level, type, attribute) VALUES (1, 'VA', 5), (1, 'CD', NULL), (NULL, 'VA', 3), (NULL, 'CD', NULL), (1, 'VA', 1);
And the sample output
LEVELS LEVEL_COUNTS TYPES TYPES_COUNTS ATTRIBUTES ATTRIBUTES_COUNTS
1 3 VA,CD 3,2 5,3,1 1,1,1
You can use the below query. The only things remaining are to add some column aliases, and to maybe add a condition to ignore rows where there is NULL.
SELECT *
FROM
(SELECT GROUP_CONCAT(lvlCount.level) as LEVELS,
GROUP_CONCAT(lvlCount.cnt) as LEVELS_COUNTS
FROM
(SELECT LEVEL,
COUNT(*) AS cnt
FROM entries where NOT(LEVEL IS NULL)
GROUP BY LEVEL
ORDER BY LEVEL DESC) AS lvlCount) AS LEVEL,
(SELECT GROUP_CONCAT(typeCount.type) as TYPES,
GROUP_CONCAT(typeCount.cnt) as TYPES_COUNTS
FROM
(SELECT TYPE,
COUNT(*) AS cnt
FROM entries where NOT(TYPE IS NULL)
GROUP BY TYPE
ORDER BY TYPE DESC) AS typeCount) AS TYPE,
(SELECT GROUP_CONCAT(attrCount.attribute) as ATTRIBUTES,
GROUP_CONCAT(attrCount.cnt) as ATTRIBUTES_COUNTS
FROM
(SELECT attribute,
COUNT(*) AS cnt
FROM entries where NOT(attribute IS NULL)
GROUP BY attribute
ORDER BY attribute DESC) AS attrCount) AS attribute;
SQLFiddle: http://sqlfiddle.com/#!2/4ea92/44

Short-circuit logic evaluation operators

Are there any short-circuit logic operators (specifically short-circuit AND and short-circuit OR) that I can use in a WHERE clause in MySQL 5.5? If there isn't, what are the alternatives?
An abstract view at my problem along with an explanation as to why I need this can be found at this fiddle:
http://sqlfiddle.com/#!2/97fd1/3
In reality we are looking at millions of books in millions of bookstores in thousands of cities in hundreds of countries, which is why we cannot accept the overhead of receiving the unneeded information with every query we dispatch and seriously need to find a way to make the evaluation stop as soon as we have all rows that satisfy the current condition, before moving on to the next OR.
Let me know if you need more information. Thanks in advance.
As requested, here is the schema used in the fiddle:
CREATE TABLE quantitycache (
id INT AUTO_INCREMENT,
quantity INT,
book_id INT NOT NULL,
bookstore_id INT NULL,
city_id INT NULL,
country_id INT NULL,
PRIMARY KEY (id)
);
As well as some example data:
INSERT INTO quantitycache
(quantity, book_id, bookstore_id, city_id, country_id)
VALUES
(5, 1, 1, NULL, NULL),
(100, 2, 1, NULL, NULL),
(7, 1, 2, NULL, NULL),
(12, 1, NULL, 1, NULL),
(12, 1, NULL, NULL, 1),
(100, 2, NULL, 1, NULL),
(100, 2, NULL, NULL, 1),
(200, 3, NULL, 1, NULL),
(250, 3, NULL, NULL, 1);
Keep in mind that a query does not execute imperatively. The query you wrote may run on multiple threads, and therefore a short-circuit operator in the where clause would not result in only one result.
Instead, use the LIMIT clause to only return the first row.
SELECT * FROM quantitycache
WHERE bookstore_id = 1 OR city_id = 1 OR country_id = 1
ORDER BY bookstore_id IS NULL ASC,
city_id IS NULL ASC,
country_id IS NULL ASC
LIMIT 1;
To get the best match for all books in a result set, save the results to a temp table, find the best result, then return interesting fields.
CREATE TEMPORARY TABLE results (id int, book_id int, match_rank int);
INSERT INTO results (id, book_id, match_rank)
SELECT id, book_id,
-- this assumes that lower numbers are better
CASE WHEN Bookstore_ID is not null then 1
WHEN City_ID is not null then 2
ELSE 3 END as match_rank
FROM quantitycache
WHERE bookstore_id = 1 OR city_id = 1 OR country_id = 1;
Select *
from (
select book_id, MIN(match_rank) as best_rank
from results
group by book_id
) as r
inner join results as rid
on r.book_id = rid.book_id
and rid.match_rank = r.best_rank
inner join quantitycache as q on q.id = rid.id;
DROP TABLE results;