(mySQL) Unable to query 2 tables properly for data - sql

I have 2 tables. One is 'page_links' and the other is 'rpp'. Table page_links is the superset of table rpp.
The following is the schema of my tables:
-- Table structure for table `page_links`
--
CREATE TABLE IF NOT EXISTS `page_links` (
`page` varchar(255) NOT NULL,
`page_link` varchar(100) NOT NULL,
`heading_id` tinyint(3) unsigned NOT NULL,
PRIMARY KEY (`page`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
--
-- Dumping data for table `page_links`
--
INSERT INTO `page_links` (`page`, `page_link`, `heading_id`) VALUES
('a1.php', 'A1', 8),
('b1.php', 'B1', 8),
('c1.php', 'C1', 5),
('d1.php', 'D1', 5),
('e1.php', 'E1', 8),
('f1.php', 'F1', 8),
('g1.php', 'G1', 8),
('h1.php', 'H1', 1),
('i1.php', 'I1', 1),
('j1.php', 'J1', 8),
('k1.php', 'K1', 8),
('l1.php', 'L1', 8),
('m1.php', 'M1', 8),
('n1.php', 'N1', 8),
('o1.php', 'O1', 8),
('p1.php', 'P1', 4),
('q1.php', 'Q1', 5),
('r1.php', 'R1', 4);
-- Table structure for table `rpp`
--
CREATE TABLE IF NOT EXISTS `rpp` (
`role_id` tinyint(3) unsigned NOT NULL,
`page` varchar(255) NOT NULL,
`is_allowed` tinyint(1) NOT NULL,
PRIMARY KEY (`role_id`,`page`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
--
-- Dumping data for table `rpp`
--
INSERT INTO `rpp` (`role_id`, `page`, `is_allowed`) VALUES
(3, 'a1.php', 1),
(3, 'b1.php', 1),
(3, 'c1.php', 1),
(3, 'd1.php', 1),
(3, 'e1.php', 1),
(3, 'f1.php', 1),
(3, 'h1.php', 1),
(3, 'i1.php', 1),
(3, 'l1.php', 1),
(3, 'm1.php', 1),
(3, 'n1.php', 1),
(4, 'a1.php', 1),
(4, 'b1.php', 1),
(4, 'q1.php', 1),
(5, 'r1.php', 1);
WHAT I AM TRYING TO DO:
I am trying to query both the above tables (in a single query) in such a way that all the pages from page_links are displayed along with the is_allowed value from rpp for a particular role. For example, I want to get the is_allowed value of all the pages from rpp for role_id = 3 and at the same time, list all the available pages from page_links. A clear example of my expected result would be:
page is_allowed role_id
----------------------------------------
a1.php 1 3
b1.php 1 3
c1.php 1 3
d1.php 1 3
e1.php 1 3
f1.php 1 3
g1.php NULL NULL
h1.php 1 3
i1.php 1 3
j1.php NULL NULL
k1.php NULL NULL
l1.php 1 3
m1.php 1 3
n1.php 1 3
o1.php NULL NULL
p1.php NULL NULL
q1.php NULL NULL
r1.php NULL NULL
One more example of my desired result could be achieved by doing a LEFT JOIN rpp ON page_links.page = rpp.page but we need to omit using role_id = 3 (or any value) to be able to get that. But I do want to specify the role_id as well and get the results. I need the query to be able to get this result. I would appreciate any replies that could help me with this. If you can suggest me any changes as well to the table(s) design to be able to achieve the desired result, that's good as well. Thanks in advance.

Use:
SELECT pl.page,
r.is_allowed,
r.role_id
FROM PAGE_LINKS pl
LEFT JOIN RPP r ON r.page = pl.page
AND OR r.role_id = 3
Previously:
SELECT pl.page,
r.is_allowed,
r.role_id
FROM PAGE_LINKS pl
LEFT JOIN RPP r ON r.page = pl.page
WHERE r.role_id IS NULL OR r.role_id = 3
This query won't return pages that have a role_id other than 3 and that aren't null.

Try this:
SELECT page_links.page, is_allowed, role_id
FROM page_links
LEFT JOIN rpp
ON rpp.page = page_links.page AND rpp.role_id = 3

SELECT page_links.page, new_rpp.is_allowed, new_rpp.role_id
FROM page_links LEFT JOIN (SELECT * FROM rpp where role_id = 3) as new_rpp USING(page)
This will give you your desired results, it will not only show the pages that correspond to role_id = 3 but will also show all the pages that don't have a corresponding entry in rpp table for role_id = 3
So this should produce the result-set that you desire.

Related

SQL Server Query to fetch nested data

I have a table like this -
declare #tmpData as table
(
MainId int,
RefId int
)
INSERT INTO #tmpData
(MainId,
RefId)
VALUES (1, NULL),
(2, 1),
(3, 2),
(4, 3),
(5, NULL),
(6, 5);
SO, If I pass a value for example - 1
then it should return all rows where value 1 is linked directly or indirectly.
And result should be - (Here 1 is ref with MainId 2, and 2 is ref with Main Id 3 and so on...) MaiId 5 and 6 is not related to 1 so output is -
Any one please provide sql server query for the same. Thanks
I tried by applying left join with same table on MainId and RefId.
But not got desired output.
You need a recursive CTE (dbfiddle)
WITH R
AS (SELECT t.MainId,
t.RefId
FROM #tmpData t
WHERE t.MainId = 1
UNION ALL
SELECT t.MainId,
t.RefId
FROM #tmpData t
JOIN R
ON t.RefId = r.MainId)
SELECT *
FROM R

fetching data from same table in multiple join hierarchy

I have a project with users, placed in some kind of hierarchy tree. a user can have multiple children and multiple parents. the organization chart looks like this :
this hierarchy uses 2 tables in database, user and position. here is the SQL to reproduce the tables with few fixtures to reproduce the organization image provided below:
-- phpMyAdmin SQL Dump
-- version 4.8.0
-- https://www.phpmyadmin.net/
--
-- Hôte : db
-- Généré le : jeu. 24 mai 2018 à 07:39
-- Version du serveur : 10.2.14-MariaDB-10.2.14+maria~jessie
-- Version de PHP : 7.2.4
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
SET AUTOCOMMIT = 0;
START TRANSACTION;
SET time_zone = "+00:00";
CREATE DATABASE IF NOT EXISTS `hierarchy` DEFAULT CHARACTER SET latin1 COLLATE latin1_swedish_ci;
USE `hierarchy`;
DROP TABLE IF EXISTS `position`;
CREATE TABLE `position` (
`id` int(11) NOT NULL,
`user_id` int(11) NOT NULL,
`parent_id` int(11) DEFAULT NULL,
`uuid` char(36) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '(DC2Type:guid)',
`type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO `position` (`id`, `user_id`, `parent_id`, `uuid`, `type`) VALUES
(2, 3, NULL, '7db54d6a-6a89-3fe2-87ba-c0dd96d4f664', 'TYPE_HR_PLUS'),
(3, 4, 3, 'adadab97-c0b8-3f89-a245-4edb077a4579', 'TYPE_HR_PLUS'),
(4, 5, 3, '4efec484-8a1f-3c27-a48f-a8c8412e18db', 'TYPE_HR_PLUS'),
(5, 6, 4, '8027b0cb-3e34-357a-8bff-80e4abbf60ef', 'TYPE_HR_PLUS'),
(6, 7, 4, 'e588ec20-9b0e-3ec6-8164-bdfafdf4b440', 'TYPE_HR'),
(7, 8, 5, '61e537d7-5ed6-36b3-8969-1913cdacb42b', 'TYPE_HR'),
(8, 9, 5, '6c241f8f-1a0a-34dc-8042-591352a764ea', 'TYPE_HR_PLUS'),
(9, 9, 3, 'e03998c4-d95a-3410-9727-099f0c83b3d5', 'TYPE_COLLABORATOR'),
(10, 10, 6, '67de2883-a7b0-3260-bb2a-67d3ffa3dc40', 'TYPE_HR'),
(11, 11, 7, '07d37612-01bf-3eec-b249-41c20d756144', 'TYPE_COLLABORATOR'),
(12, 11, 5, '09f7f6f1-59e2-37c0-a2e4-ae9b58ba6c93', 'TYPE_HR'),
(13, 12, 9, '724b8185-b6eb-3d35-b71f-1d076b3020ed', 'TYPE_HR'),
(14, 13, 9, '97d90e5c-7872-3e1e-be65-0f6b5329f54c', 'TYPE_HR_PLUS'),
(15, 14, 10, '3c357b9d-e162-316b-921c-25157d523e70', 'TYPE_COLLABORATOR'),
(16, 14, 3, '542235c6-462c-4922-a629-303430701000', 'TYPE_COLLABORATOR'),
(17, 15, 10, '2d208936-a7e9-32c1-963f-0df7f57ae463', 'TYPE_COLLABORATOR'),
(18, 16, 11, '00b678a2-54cd-3f37-9381-0cd92acea079', 'TYPE_COLLABORATOR'),
(19, 17, 8, 'ec4b8cad-fbce-3692-8d4c-f48f7ffa6452', 'TYPE_COLLABORATOR'),
(20, 18, 12, '48f0c85c-3801-3275-8978-5b2573fd7e0b', 'TYPE_COLLABORATOR'),
(21, 18, 10, 'd6129dec-8823-3304-8dfe-819118073d1f', 'TYPE_COLLABORATOR'),
(22, 19, 13, '1ec9c202-97d7-3311-9119-f6e9f55d058b', 'TYPE_COLLABORATOR'),
(23, 20, 9, '3ab32120-520b-3d41-80f0-56fd3876eecb', 'TYPE_COLLABORATOR');
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL,
`uuid` char(36) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '(DC2Type:guid)',
`first_name` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
`last_name` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO `user` (`id`, `uuid`, `first_name`, `last_name`) VALUES
(3, 'fb7304f6-5ade-4c06-83e5-36827a4d7904', 'Albus', 'Dumbledore'),
(4, '60af55ca-e9cd-3d8a-bc8a-c9f7ec99baa1', 'Minerva', 'McGonagall'),
(5, 'a422b364-b68f-324b-8836-d38ca62001f5', 'Severus', 'Rogue'),
(6, 'cbdf94a8-f4e3-360b-ba7f-10ea7e59b85d', 'Filius', 'Flitwick'),
(7, '9bce88c9-7b7d-3db3-8780-b6397bb1263d', 'Remus', 'Lupin'),
(8, 'b9d85cfa-3c46-33c1-8750-53be1c281111', 'Pomona', 'Chourave'),
(9, 'c07d10d7-333f-327a-ab32-201209127b7a', 'Rolanda', 'Bibine'),
(10, '8a7b76a1-5f0c-31ff-a93a-ca478598682b', 'Gilderoy', 'Lockhart'),
(11, '7302700a-0bc5-3ed0-84fc-857e975a5771', 'Alastor', 'Maugrey'),
(12, '7b8722f3-1570-3351-864a-ede9ee3a90e6', 'Sybille', 'Trelawney'),
(13, '12a7683a-00da-46a6-b7fd-a6eae5b10e3c', 'Dolores', 'Ombrage'),
(14, 'f054bdeb-a8b8-41e5-be78-b00d1b9377f3', 'Harry', 'Potter'),
(15, 'b1f696ad-cfa3-3830-8685-c45ff4a4ffda', 'Ronald', 'Weasley'),
(16, '8b2a64e9-ed28-363d-a731-3e15e255cd1d', 'Hermione', 'Granger'),
(17, 'f6aa74f9-2a4a-346d-bf75-3f8b02614c19', 'Neville', 'Londubat'),
(18, '8fbbc431-2555-3718-8055-ad8637260732', 'Drago', 'Malefoy'),
(19, '78a58546-8ae1-330d-9426-c23c74b71e78', 'Luna', 'Lovegood'),
(20, '4739a44b-d1b7-3390-9bbb-cbd2e39b4ec0', 'Ginny', 'Weasley');
ALTER TABLE `position`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `uuid_idx` (`uuid`),
ADD KEY `IDX_462CE4F5727ACA70` (`parent_id`),
ADD KEY `IDX_462CE4F5A76ED395` (`user_id`);
ALTER TABLE `user`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `uuid_idx` (`uuid`);
ALTER TABLE `position`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1024;
ALTER TABLE `user`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1027;
ALTER TABLE `position`
ADD CONSTRAINT `FK_462CE4F5727ACA70` FOREIGN KEY (`parent_id`) REFERENCES `user` (`id`),
ADD CONSTRAINT `FK_462CE4F5A76ED395` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`);
COMMIT;
One important thing to notice is the position table represents a relation between a user and its parent, and it also stores a string defining the relation type (actually it represents the user type of the child in this relation).
EDIT : one really important thing I didn't mention yet is there is no recursivity in this tree, there can be only a maximum number of 5 levels, as shown in the organization image: 1 HR_PLUS Head of Department, followed by maximum 3 levels of HR_PLUS or HR, followed by 1 level of COLLAB
What I'm trying to accomplish is the following:
For a given position uuid, i'd like to fetch every child user below this relation.
For example, the relation between Minerva McGonagall and Albus Dumbledore is the position #3 with uuid adadab97-c0b8-3f89-a245-4edb077a4579. Given this uuid, i'd like to fetch every user below Minerva McGonagall:
+---------------------------------------+
| id | first_name | last_name |
+---------------------------------------+
| 6 | Filius | Flitwick |
| 7 | Remus | Lupin |
| 10 | Gilderoy | Lockhart |
| 11 | Alastor | Maugrey |
| 14 | Harry | Potter |
| 15 | Ronald | Weasley |
| 16 | Hermione | Granger |
| 18 | Drago | Malefoy |
+---------------------------------------+
The query I wrote so far is the following:
SELECT u2.id, u2.first_name, u2.last_name
FROM `user` u1
JOIN position p1 ON p1.user_id = u1.id
JOIN position p2 ON p2.parent_id = p1.user_id
JOIN user u2 ON u2.id = p2.user_id
WHERE p1.uuid = 'adadab97-c0b8-3f89-a245-4edb077a4579'
AND p1.type IN ('TYPE_HR', 'TYPE_HR_PLUS')
I added a filter on type property because I don't want the query to return anything if type is not HR or HR_PLUS.
Right now, the query returns the users directly below Minerva McGonagall (Filius and Remus). And I can't figure how to return the rest of the users below them.
any help would be greatly appreciated.
I just succeed in fetch the expected data using a UNION, as this is not a recursive tree:
SELECT u2.id, u2.first_name, u2.last_name
FROM `user` u1
JOIN position p1 ON p1.user_id = u1.id
JOIN position p2 ON p2.parent_id = p1.user_id
JOIN user u2 ON u2.id = p2.user_id
WHERE p1.uuid = 'adadab97-c0b8-3f89-a245-4edb077a4579'
AND p1.type IN ('TYPE_HR', 'TYPE_HR_PLUS')
UNION
SELECT u2.id, u2.first_name, u2.last_name
FROM `user` u1
JOIN position p1 ON p1.user_id = u1.id
JOIN position p2 ON p2.parent_id = p1.user_id
JOIN position p3 ON p3.parent_id = p2.user_id
JOIN user u2 ON u2.id = p3.user_id
WHERE p1.uuid = 'adadab97-c0b8-3f89-a245-4edb077a4579'
AND p1.type IN ('TYPE_HR', 'TYPE_HR_PLUS')
UNION
SELECT u2.id, u2.first_name, u2.last_name
FROM `user` u1
JOIN position p1 ON p1.user_id = u1.id
JOIN position p2 ON p2.parent_id = p1.user_id
JOIN position p3 ON p3.parent_id = p2.user_id
JOIN position p4 ON p4.parent_id = p3.user_id
JOIN user u2 ON u2.id = p4.user_id
WHERE p1.uuid = 'adadab97-c0b8-3f89-a245-4edb077a4579'
AND p1.type IN ('TYPE_HR', 'TYPE_HR_PLUS')
but this looks very heavy , and i'm pretty sure there is a simpler way to do it.
Since you have already found the union solution, here's an alternative using a loop (And pseudocode):
Create a table to hold your results, lets call it r.
Insert into r the results of your first query, along with a field named level with a value of 1.
set i=2
while (at least a line of r exists where level=i-1) do the following:
insert into r (level=i)
a modification of your select based on selecting positions from r
where level=i-1
Is this what you're looking for? DB Fiddle
with recursive cte as (
select `user_id` as `ancestor_id`
/* , 0 as `degreesOfSeperation` */
, `user_id`
from `position`
union all
select c.`ancestor_id`
/* , c.`degreesOfSeperation` + 1 */
, p.`user_id`
from `cte` as c
inner join `position` as p
on p.`parent_id` = c.`user_id`
)
select u.id, u.first_name, u.last_name
from cte c
inner join `user` a on a.`id` = c.`ancestor_id`
inner join `position` p on p.`user_id` = a.`id`
inner join `user` u on u.`id` = c.`user_id`
/* WHERE c.`degreesOfSeperation` > 0 */
WHERE c.`user_id` != c.`ancestor_id`
AND p.`uuid` = 'adadab97-c0b8-3f89-a245-4edb077a4579'
AND p.`type` IN ('TYPE_HR', 'TYPE_HR_PLUS')
order by c.`degreesOfSeperation`
;
Output
id first_name last_name
Id | First_Name | Last_Name
---+------------+----------
6 | Filius | Flitwick
7 | Remus | Lupin
10 | Gilderoy | Lockhart
11 | Alastor | Maugrey
18 | Drago | Malefoy
14 | Harry | Potter
15 | Ronald | Weasley
16 | Hermione | Granger
Most of this is likely already already familiar to you.
The logic to deal with the hierarchical structure is the recursive cte.
This has 2 parts:
Part 1
select user_id as ancestor_id
, 0 as degreesOfSeperation
, user_id
from position
This gets all users from the position table, setting their record as the ancestor record; i.e. the root from which we'll grow our tree. We set degreesOfSeperation to 0 to state that this is the root record; i.e. the one where the user and the ancestor are one and the same.
Part 2
select c.`ancestor_id`
, c.`degreesOfSeperation` + 1
, p.`user_id`
from `cte` as c
inner join `position` as p
on p.`parent_id` = c.`user_id`
This is the recursive part; for each user added to the tree already (i.e. from the root statement; or from the last iteration of populating the cte table) we now fetch any other positions for which the position's parents match the previous iteration's user. We add one to the degreesOfSeparation so we can tell how far away from the root ancestor this record is.
DegressOfSeparation is not actually required (in fact, in the above statement I've commented this out); but I tend to include it since it's useful for debugging / seeing what's going on. Here's what the cte looks like without any filters, to help you understand what's going on:
With recursive cte as (
select `user_id` as `ancestor_id`
, 0 as `degreesOfSeperation`
, `user_id`
from `position`
union all
select c.`ancestor_id`
, c.`degreesOfSeperation` + 1
, p.`user_id`
from `cte` as c
inner join `position` as p
on p.`parent_id` = c.`user_id`
)
select *
from cte c
order by c.`ancestor_id`
, c.`degreesOfSeperation`
, c.`user_id`
;
Results here

How to retrieve data with group by aggregate function in SQL Server 2012

I am new to SQL Server 2012. This is my table DDL & DML script.
CREATE TABLE [tbl_item_i18n]
(
[item_id] [int] NOT NULL,
[lang_id] [int] NOT NULL,
[item_text] [nvarchar](max) NULL
);
INSERT INTO [tbl_item_i18n] ([item_id],[lang_id],[item_text])
VALUES (1, 1, 'item1'), (1, 2, 'idem 1'),
(2, 1, 'item2'),
(3, 1, 'item3'), (3, 2, 'idem 3'),
(4, 1, 'item4'), (4, 2, 'idem 4');
My expected output is :
This is what I have tried :
select
lang_id,
case when lang_id = 2 AND itemI18N.item_text is not null then itemI18N.item_text
when lang_id = 1 then itemI18N.item_text
end as ite_texte
from
tbl_item_i18n itemI18N
group by
itemI18N.item_id, lang_id, itemI18N.item_text
But it does not give me expected result.
Purpose :- I would like to retrieve data for lang_id = 2. If the record for lang_id = 2 does not exist, then retrieve data for lang_id = 2.
How do I retrieve data using aggregate function?
LEFT JOIN Bring all the column with lenguaje 1, and null if doesnt have lenguaje 2.
I include extra column so you understand the result.
SELECT
item_id,
CASE
WHEN B.lang_id IS NULL THEN A.item_text
ELSE B.item_text
END as item_name,
A.*
B.*
FROM tbl_item_i18n A
LEFT JOIN tbl_item_i18n B
ON A.item_id = B.item_id
AND A.lang_id < B.lang_id
NOTE
Maybe need especial consideration if more than 2 lenguajes.
Another solution
SELECT *
FROM
(
SELECT item_id, lang_id, item_text as item_name,
ROW_NUMBER() over (partition by item_id order by lang_id desc) as RN
FROM tbl_item_i18n
) as t
WHERE RN = 1

How to create an "on-the-fly" mapping table within a SELECT statement in Postgresql

I'm creating a select statement that combines two tables, zone and output,
based on a referenced device table and on a mapping of zone_number to output_type_id.
The mapping of zone_number to output_type_id doesn't appear
anywhere in the database, and I would like to create it "on-the-fly" within the select
statement. Below is my schema:
CREATE TABLE output_type (
id INTEGER NOT NULL,
name TEXT,
PRIMARY KEY (id)
);
CREATE TABLE device (
id INTEGER NOT NULL,
name TEXT,
PRIMARY KEY (id)
);
CREATE TABLE zone (
id SERIAL NOT NULL,
device_id INTEGER NOT NULL REFERENCES device(id),
zone_number INTEGER NOT NULL,
PRIMARY KEY (id),
UNIQUE (zone_number)
);
CREATE TABLE output (
id SERIAL NOT NULL,
device_id INTEGER NOT NULL REFERENCES device(id),
output_type_id INTEGER NOT NULL REFERENCES output_type(id),
enabled BOOLEAN NOT NULL,
PRIMARY KEY (id)
);
And here is some example data:
INSERT INTO output_type (id, name) VALUES
(101, 'Output 1'),
(202, 'Output 2'),
(303, 'Output 3'),
(404, 'Output 4');
INSERT INTO device (id, name) VALUES
(1, 'Test Device');
INSERT INTO zone (device_id, zone_number) VALUES
(1, 1),
(1, 2),
(1, 3),
(1, 4);
INSERT INTO output (device_id, output_type_id, enabled) VALUES
(1, 101, TRUE),
(1, 202, FALSE),
(1, 303, FALSE),
(1, 404, TRUE);
I need to get the associated enabled field from the output table for each zone for a given device.
Each zone_number maps to an output_type_id. For this example:
zone_number | output_type_id
----------------------------
1 | 101
2 | 202
3 | 303
4 | 404
One way to handle the mapping would be to create a new table
CREATE TABLE zone_output_type_map (
zone_number INTEGER,
output_type_id INTEGER NOT NULL REFERENCES output_type(id)
);
INSERT INTO zone_output_type_map (zone_number, output_type_id) VALUES
(1, 101),
(2, 202),
(3, 303),
(4, 404);
And use the following SQL to get all zones, plus the enabled flag, for device 1:
SELECT zone.*, output.enabled
FROM zone
JOIN output
ON output.device_id = zone.device_id
JOIN zone_output_type_map map
ON map.zone_number = zone.zone_number
AND map.output_type_id = output.output_type_id
AND zone.device_id = 1
However, I'm looking for a way to create the mapping of zone nunbers to output
types without creating a new table and without piecing together a bunch of AND/OR
statements. Is there an elegant way to create a mapping between the two fields
within the select statement? Something like:
SELECT zone.*, output.enabled
FROM zone
JOIN output
ON output.device_id = zone.device_id
JOIN (
SELECT (
1 => 101,
2 => 202,
3 => 303,
4 => 404
) (zone_number, output_type_id)
) as map
ON map.zone_number = zone.zone_number
AND map.output_type_id = output.output_type_id
AND zone.device_id = 1
Disclaimer: I know that ideally the enabled field would exist in the zone
table. However, I don't have control over that piece. I'm just looking for the
most elegant solution from the application side. Thanks!
You can use VALUES as an inline table and JOIN to it, you just need to give it an alias and column names:
join (values (1, 101), (2, 202), (3, 303), (4, 304)) as map(zone_number, output_type_id)
on ...
From the fine manual:
VALUES can also be used where a sub-SELECT might be written, for
example in a FROM clause:
SELECT f.*
FROM films f, (VALUES('MGM', 'Horror'), ('UA', 'Sci-Fi')) AS t (studio, kind)
WHERE f.studio = t.studio AND f.kind = t.kind;
UPDATE employees SET salary = salary * v.increase
FROM (VALUES(1, 200000, 1.2), (2, 400000, 1.4)) AS v (depno, target, increase)
WHERE employees.depno = v.depno AND employees.sales >= v.target;
So just to complement the accepted answer, the following code is a valid, self-contained Postgresql expression which will evaluate to an 'inline' relation with columns (zone_number, output_type_id):
SELECT * FROM
(VALUES
(1, 101),
(2, 202),
(3, 303),
(4, 304)
) as i(zone_number, output_type_id)
(The (VALUES ... AS ...) part alone will not make a valid expression, which is why I added the SELECT * FROM.)
JOIN
(SELECT 1 zone_number, 101 as output_type_id
UNION ALL
SELECT 2 zone_number, 202 as output_type_id
UNION ALL
SELECT 3 zone_number, 303 as output_type_id
) mappings on mappings.zone_number = zone.zone_number

Arel causing infinite loop on aggregation

I have trouble with using Arel to aggregate 2 columns in the same query. When I run this, the whole server freezes for a minute, before the rails dev-server crashes. I suspect an infinite loop :).
Maybe I have misunderstood the whole concept of Arel, and I would be grateful if anybody could have a look at it.
The expected result of this query is something like this:
[{:user_id => 1, :sum_account_charges => 300, :sum_paid_debts => 1000},...]
a_account_charges = Table(:account_charges)
a_paid_debts = Table(:paid_debts)
a_participants = Table(:expense_accounts_users)
account_charge_sum = a_account_charges
.where(a_account_charges[:expense_account_id].eq(id))
.group(a_account_charges[:user_id])
.project(a_account_charges[:user_id], a_account_charges[:cost].sum)
paid_debts_sum = a_paid_debts
.where(a_paid_debts[:expense_account_id].eq(id))
.group(a_paid_debts[:from_user_id])
.project(a_paid_debts[:from_user_id], a_paid_debts[:cost].sum)
charges = a_participants
.where(a_participants[:expense_account_id].eq(id))
.join(account_charge_sum)
.on(a_participants[:user_id].eq(account_charge_sum[:user_id]))
.join(paid_debts_sum)
.on(a_participants[:user_id].eq(paid_debts_sum[:from_user_id]))
I'm new to arel, but after banging on this for several days and really digging, I don't think it can be done. Here's an outline of what I've done, if anyone has any additional insight it would be welcome.
First, these scripts will create the test tables and populate them with test data. I have set up 9 expense_account_users, each with a different set of charges/paid_debts, as follows: 1 charge/1 payment, 2 charges/2 payments, 2 charges/1 payment, 2 charges/0 payments, 1 charge/2 payments, 0 charges/2 payments, 1 charge/0 payments, 0 charges/1 payment, 0 charges, 0 payments.
CREATE TABLE IF NOT EXISTS `expense_accounts_users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`expense_account_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=10 ;
INSERT INTO `expense_accounts_users` (`id`, `expense_account_id`) VALUES (1, 1), (2, 1), (3, 1), (4, 1), (5, 1), (6, 1), (7, 1), (8, 1), (9, 1);
CREATE TABLE IF NOT EXISTS `account_charges` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`expense_account_id` int(11) DEFAULT NULL,
`user_id` int(11) DEFAULT NULL,
`cost` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=10 ;
INSERT INTO `account_charges` (`id`, `expense_account_id`, `user_id`, `cost`) VALUES (1, 1, 1, 1), (2, 1, 2, 1), (3, 1, 2, 2), (4, 1, 3, 1), (5, 1, 3, 2), (6, 1, 4, 1), (7, 1, 5, 1), (8, 1, 5, 2), (9, 1, 7, 1);
CREATE TABLE IF NOT EXISTS `paid_debts` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`expense_account_id` int(11) DEFAULT NULL,
`user_id` int(11) DEFAULT NULL,
`cost` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=10 ;
INSERT INTO `paid_debts` (`id`, `expense_account_id`, `user_id`, `cost`) VALUES (1, 1, 1, 1), (2, 1, 2, 1), (3, 1, 2, 2), (4, 1, 3, 1), (5, 1, 4, 1), (6, 1, 4, 2), (7, 1, 6, 1), (8, 1, 6, 2), (9, 1, 8, 1);
Ultimately, to get the data that you are after in one fell swoop, this is the SQL statement you'd use:
SELECT user_charges.user_id,
user_charges.sum_cost,
COALESCE(SUM(paid_debts.cost), 0) AS 'sum_paid'
FROM (
SELECT expense_accounts_users.id AS 'user_id',
COALESCE(sum(account_charges.cost), 0) AS 'sum_cost'
FROM expense_accounts_users
LEFT OUTER JOIN account_charges on expense_accounts_users.id = account_charges.user_id
GROUP BY expense_accounts_users.id)
AS user_charges
LEFT OUTER JOIN paid_debts ON user_charges.user_id = paid_debts.user_id
GROUP BY user_charges.user_id
You have to do a LEFT OUTER JOIN between users and charges first so that you get a row for every user, then you have to LEFT OUTER JOIN the result to debts in order to avoid multiplying your results with the two joins inside the same construct.
(note the use of COALESCE to convert NULL values from the LEFT OUTER JOINs to zeroes - a convenience item, perhaps)
The result of this statement is this:
user_id sum_cost sum_paid
1 1 1
2 3 3
3 3 1
4 1 3
5 3 0
6 0 3
7 1 0
8 0 1
9 0 0
After many attempts, I found that this arel code came the closest to what we are after:
c = Arel::Table.new(:account_charges)
d = Arel::Table.new(:paid_debts)
p = Arel::Table.new(:expense_accounts_users)
user_charges = p
.where(p[:expense_account_id].eq(1))
.join(c, Arel::Nodes::OuterJoin)
.on(p[:id].eq(c[:user_id]))
.project(p[:id], c[:cost].sum.as('sum_cost'))
.group(p[:id])
charges = user_charges
.join(d, Arel::Nodes::OuterJoin)
.on(p[:id].eq(d[:user_id]))
.project(d[:cost].sum.as('sum_paid'))
Essentially, I am joining users to charges in the first construct with a LEFT OUTER JOIN, then trying to take the result of that and LEFT OUTER JOIN it back to debts. This arel code produces the following SQL statement:
SELECT `expense_accounts_users`.`id`,
SUM(`account_charges`.`cost`) AS sum_cost,
SUM(`paid_debts`.`cost`) AS sum_paid
FROM `expense_accounts_users`
LEFT OUTER JOIN `account_charges` ON `expense_accounts_users`.`id` = `account_charges`.`user_id`
LEFT OUTER JOIN `paid_debts` ON `expense_accounts_users`.`id` = `paid_debts`.`user_id`
WHERE `expense_accounts_users`.`expense_account_id` = 1
GROUP BY `expense_accounts_users`.`id`
Which, when run, produces this output:
id sum_cost sum_paid
1 1 1
2 6 6
3 3 2
4 2 3
5 3 NULL
6 NULL 3
7 1 NULL
8 NULL 1
9 NULL NULL
Very close, but not quite. First, the lack of COALESCE gives us NULL values instead of zeroes - I'm not sure how to effect the COALESCE function call from within arel.
More importantly, though, combining the LEFT OUTER JOINs into one statement without the internal subselect is resulting in the sum_paid totals getting multiplied up in examples 2, 3 and 4 - any time there is more than one of either charge or payment and at least one of the other.
Based on some online readings, I had hoped that changing the arel slightly would solve the problem:
charges = user_charges
.join(d, Arel::Nodes::OuterJoin)
.on(user_charges[:id].eq(d[:user_id]))
.project(d[:cost].sum.as('sum_paid'))
But any time I used user_charges[] in the second arel construct, I got an undefined method error for SelectManager#[]. This may be a bug, or may be right - I really can't tell.
I just don't see that arel has a way to make use of the SQL from the first construct as a queryable object in the second construct with the necessary subquery alias, as is needed to make this happen in one SQL statement.