Short-circuit logic evaluation operators - mysql

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;

Related

Unexpected LEFT JOIN behavior with condition in ON clause

I have the following three tables:
fees - has information about a fee on a bill
payments - has information about payments on a bill (including payments to fees)
details - has detailed information about the payment
I'm struggling to understand why my query isn't returning the result I expect.
SQL Fiddle: sqlfiddle.com/#!9/942636/3
CREATE TABLE fees (
receipt_number int(11) NOT NULL,
bill_number int(11) NOT NULL,
version int(11) NOT NULL,
fee_id int(11) NOT NULL,
fee_type varchar(30) NOT NULL
);
CREATE TABLE payments (
receipt_number int(11) NOT NULL,
bill_number int(11) NOT NULL,
version int(11) NOT NULL,
payment_id int(11) NOT NULL,
amount decimal(13,2) NOT NULL DEFAULT '0.00'
);
CREATE TABLE details (
receipt_number int(11) NOT NULL,
payment_id int(11) NOT NULL,
fee_type varchar(30) DEFAULT NULL
amount decimal(13,2) NOT NULL DEFAULT '0.00'
);
INSERT INTO fees (receipt_number, bill_number, version, fee_id, fee_type)
VALUES (111, 100, 1, 1, 'a'),
(111, 100, 1, 1, 'b'),
(111, 100, 1, 2, 'c'),
(111, 100, 1, 2, 'd');
INSERT INTO payments (receipt_number, bill_number, version, payment_id, amount)
VALUES (111, 100, 1, 98, 30.00),
(111, 100, 1, 99, 60.00);
INSERT INTO details (receipt_number, payment_id, fee_type, amount)
VALUES (111, 98, 'a', 10.00),
(111, 98, 'b', 10.00),
(111, 98, 'd', 10.00),
(111, 99, 'a', 20.00),
(111, 99, 'b', 20.00),
(111, 99, 'c', 20.00);
I'm attempting to find out if:
A bill has a fee with type 'c'
There is a payment made towards that fee (determined by the fee_type)
My query:
SELECT fees.bill_number, details.receipt_number AS has_payment_type_c
FROM fees
LEFT JOIN payments
USING (bill_number, version)
LEFT JOIN details
ON details.receipt_number = payments.receipt_number
AND details.payment_id = payments.payment_id
AND details.fee_type = 'c'
WHERE fees.fee_type = 'c'
AND details.receipt_number IS NULL;
I get the result:
bill_number has_payment_type_c
100 (null)
I should not see the bill_number in this list, as the bill has a fee and a payment towards that fee.
I am filtering the details.fee_type in the ON clause to both reduce the number of records looked up in that table and to only join to that table for that specific fee_type. The query seems to "work" (0 results) by moving the fee_type join condition to the WHERE clause, but I don't think that is correct.
My questions:
What am I doing wrong in my query? How can I fix it to produce the result I'm looking for, and how does that fix work?
Assuming I get a working query, can I just simply reverse the table order and the SELECT to find the opposite information - payments of a certain fee_type without an associated fee record of that same type?
The answer is to use a derived table that inner joins payments and details, and then left join the derived table to fees:
SELECT DISTINCT fees.bill_number, fees.version, x.bill_number AS has_payment_type_c
FROM fees
LEFT JOIN (
SELECT payments.bill_number, payments.version, details.fee_type
FROM payments
JOIN details
USING(receipt_number, payment_id)
WHERE details.fee_type = 'c'
) x
ON fees.bill_number = x.bill_number
AND fees.version = x.version
WHERE fees.fee_type = 'c'
AND x.bill_number IS NULL;
This will return the expected 0 results, and can be validated by removing the where condition that checks for a null x.bill_number:
bill_number version has_payment_type_c
100 1 100
The same approach works for finding payments without a fee. Use a derived table that inner joins payments and details, then left join to fees. SELECT the fee.bill_number AS has_payment_type_c and add WHERE fee.bill_number IS NULL to the WHERE clause.

How to count multiple columns grouping by rows in 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

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

Self-referencing MySQL table

I have a table:
CREATE TABLE IF NOT EXISTS `columns` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`section_id` int(11) NOT NULL,
`columntype` int(11) NOT NULL,
`name` varchar(128) COLLATE utf8_bin NOT NULL,
`refer_type` int(11) NOT NULL,
PRIMARY KEY (`id`));
with the following content:
INSERT INTO `columns` (`id`, `section_id`, `columntype`, `name`, `refer_type`) VALUES
(9, 35, 1, 'Headline1', 0),
(10, 36, 1, 'Headline2', 0),
(11, 36, 5, 'Headline3', 10),
(12, 36, 3, 'Headline4', 0),
(13, 36, 4, 'Headline5', 10);
The column refer_from contains info on which other column the containing data must be pulled from and calculated with. When Headline2 is changed in my webapp, the content of Headline3 and Headline5 should be changed. I'm performing the final calculations in the webapp, but I need a SQL that will give me a result like
id name columntype refer_type
10 Headline2 1 4, 5
11 Headline3 5 null
12 Headline4 3 null
13 Headline5 4 null
I've tried with something like SELECT id, name, columntype, (SELECT GROUP_CONCAT(refer_type SEPARATOR "," )... but that gave me 4,5 on all four result rows.
Thanks in advance!
If I get it right, you might need something like this select:
SELECT c.id, c.name, c.columntype, ref.reftypes
FROM columns c LEFT JOIN
(SELECT GROUP_CONCAT(columntype SEPARATOR "," ) reftypes, refer_type
FROM columns
GROUP BY refer_type) ref
ON c.id = ref.refer_type;
This does it:
SELECT c1.id, c1.name, c1.columntype, GROUP_CONCAT(c2.columntype ORDER BY c2.columntype) AS refer_type
FROM columns AS c1
LEFT JOIN columns AS c2 ON c1.id = c2.refer_type
GROUP BY c1.id
ORDER BY c1.id
DEMO
Thanks! Both SQLs works great! I've selected Bamar's answer because of the simplicity.

Retrieve one id referred by two ids

I'm stuck with the following problem:
SQL query for the table:
CREATE TABLE IF NOT EXISTS `thread_users` (
`thread_id` bigint(20) unsigned NOT NULL,
`user_id` bigint(20) unsigned NOT NULL,
PRIMARY KEY (`thread_id`,`user_id`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Let's say that I have those data:
INSERT INTO `thread_users` (`thread_id`, `user_id`) VALUES
(1, 1),
(1, 2),
(2, 1),
(2, 2),
(2, 3),
(3, 1),
(3, 4);
I need to retrieve the thread_id referred by only 2 ids: X & Y (both known).
With the above data, I want to be able to retrieve the thread_id where only user_id = 1 & user_id = 2 are present.
What i Know for sure about this table:
If a thread is composed by only 2 users, there is no other threads containing only those two ids. (It's check outside mysql before the insertion)
A user can't be present in a thread more than once. (primary key)
What i have thinking of to resolve this problem:
Sum up (user_id 1 + user_id 2) search for SUMs equal to that result + (user_id = X OR user_id = Y). But i haven't been able to write correctly this query AND I also need to check the number of user_id in that thread...
Obviously: searching id where the number of user_id on threads are equal to 2 and where user_id are equals to X & Y.
Thanks for the help guys!
SELECT tu1.thread_id
FROM thread_users AS tu1
INNER JOIN thread_users AS tu2
ON tu1.thread_id = tu2.thread_id
AND tu1.user_id <> tu2.user_id
LEFT OUTER JOIN thread_users AS tu3
ON tu1.thread_id = tu3.thread_id
AND tu1.user_id <> tu3.user_id
AND tu2.user_id <> tu3.user_id
WHERE tu1.user_id = 1
AND tu2.user_id = 2
AND tu3.user_id IS NULL
Something like this
SELECT thread_id FROM thread_users WHERE user_id IN(1,2) GROUP BY thread_id
HAVING COUNT(user_id)=2
SQL fiddle
I think this is a bit simpler than the JOIN example, and also a bit faster:
SELECT `thread_id`, GROUP_CONCAT(`user_id`) AS this_match FROM `thread_users`
GROUP BY `thread_id` HAVING this_match = '1,2'