Only select from tables that are of interest - sql

SQL Server 2016
I have a number of tables
Table A Table B Table C Table D
User | DataA User | DataB User | DataC User | DataD
=========== =========== =================== =============
1 | 10 1 | 'hello' 4 | '2020-01-01' 1 | 0.34
2 | 20 2 | 'world'
3 | 30
So some users have data for A,B,C and/or D.
Table UserEnabled
User | A | B | C | D
=============================
1 | 1 | 1 | 0 | 0
2 | 1 | 1 | 0 | 0
3 | 1 | 0 | 0 | 0
4 | 0 | 0 | 1 | 0
Table UserEnabled indicates whether we are interested in any of the data in the corresponding tables A,B,C and/or D.
Now I want to join those tables on User but I do only want the columns where the UserEnabled table has at least one user with a 1 (ie at least one user enabled). Ideally I only want to join the tables that are enabled and not filter the columns from the disabled tables afterwards.
So as a result for all users I would get
User | DataA | DataB | DataC
===============================
1 | 10 | 'hello' | NULL
2 | 20 | 'world' | NULL
3 | 30 | NULL | NULL
4 | NULL | NULL | '2020-01-01'
No user has D enabled so it does not show up in a query
I was going to come up with a dynamic SQL that's built every time I execute the query depending on the state of UserEnabled but I'm afraid this is going to perform poorly on a huge data set as the execution plan will need to be created every time. I want to dynamically display only the enabled data, not columns with all NULL.
Is there another way?
Usage will be a data sheet that may be generated up to a number of times per minute.

You have no choice but to approach this through dynamic SQL. A select query has a fixed set of columns defined when the query is created. No such thing as "variable" columns.
What can you do? One method is to "play a trick". Store the columns as JSON (or XML) and delete the empty columns.
Another method is to create a view that has the specific logic you need. I think you can maintain this view by altering it in a trigger, based on when data in the enabled table changes. That said, altering the view requires dynamic SQL so the code will not be pretty.

Just because I thought this could be fun.
Example
Declare #Col varchar(max) = ''
Declare #Src varchar(max) = ''
Select #Col = #Col+','+Item+'.[Data'+Item+']'
,#Src = #Src+'Left Join [Table'+Item+'] '+Item+' on U.[User]=['+Item+'].[User] and U.['+Item+']=1'+char(13)
From (
Select Item
From ( Select A=max(A)
,B=max(B)
,C=max(C)
,D=max(D)
From UserEnabled
Where 1=1 --<< Use any Key Inital Filter Condition Here
) A
Unpivot ( value for item in (A,B,C,D)) B
Where Value=1
) A
Declare #SQL varchar(max) = '
Select U.[User]'+#Col+'
From #UserEnabled U
'+#Src
--Print #SQL
Exec(#SQL)
Returns
User DataA DataB DataC
1 10 Hello NULL
2 20 World NULL
3 30 NULL NULL
4 NULL NULL 2020-01-01
The Generated SQL
Select A.[User],A.[DataA],B.[DataB],C.[DataC]
From UserEnabled U
Left Join TableA A on U.[User]=[A].[User] and U.[A]=1
Left Join TableB B on U.[User]=[B].[User] and U.[B]=1
Left Join TableC C on U.[User]=[C].[User] and U.[C]=1

If all the relations are 1:1, you can make one query with
...
FROM u
LEFT JOIN a ON u.id = a.u_id
LEFT JOIN b ON u.id = b.u_id
LEFT JOIN c ON u.id = c.u_id
LEFT JOIN d ON u.id = d.u_id
...
and use display logic on the client to omit the irrelevant columns.
If more than one relation is 1:N, then you'd likely have to do multiple queries anyway to prevent N1xN2 results.

Related

SQL - Set column value to the SUM of all references

I want to have the column "CurrentCapacity" to be the SUM of all references specific column.
Lets say there are three rows in SecTable which all have FirstTableID = 1. Size values are 1, 1 and 3.
The row in FirstTable which have ID = 1 should now have a value of 5 in the CurrentCapacity column.
How can I make this and how to do automatically on insert, update and delete?
Thanks!
FirstTable
+----+-------------+-------------------------+
| ID | MaxCapacity | CurrentCapacity |
+----+-------------+-------------------------+
| 1 | 5 | 0 (desired result = 5) |
+----+-------------+-------------------------+
| 2 | 5 | 0 |
+----+-------------+-------------------------+
| 3 | 5 | 0 |
+----+-------------+-------------------------+
SecTable
+----+-------------------+------+
| ID | FirstTableID (FK) | Size |
+----+-------------------+------+
| 1 | 1 | 2 |
+----+-------------------+------+
| 2 | 1 | 3 |
+----+-------------------+------+
In general, a view is a better solution than trying to keep a calculated column up-to-date. For your example, you could use this:
CREATE VIEW capacity AS
SELECT f.ID, f.MaxCapacity, COALESCE(SUM(s.Size), 0) AS CurrentCapacity
FROM FirstTable f
LEFT JOIN SecTable s ON s.FirstTableID = f.ID
GROUP BY f.ID, f.MaxCapacity
Then you can simply
SELECT *
FROM capacity
to get the results you desire. For your sample data:
ID MaxCapacity CurrentCapacity
1 5 5
2 5 0
3 5 0
Demo on SQLFiddle
Got this question to work with this trigger:
CREATE TRIGGER UpdateCurrentCapacity
ON SecTable
AFTER INSERT, UPDATE, DELETE
AS
BEGIN
SET NOCOUNT ON
DECLARE #Iteration INT
SET #Iteration = 1
WHILE #Iteration <= 100
BEGIN
UPDATE FirstTable SET FirstTable.CurrentCapacity = (SELECT COALESCE(SUM(SecTable.Size),0) FROM SecTable WHERE FirstTableID = #Iteration) WHERE ID = #Iteration;
SET #Iteration = #Iteration + 1
END
END
GO
Personally, I would not use a trigger either or store CurrentCapacity as a value since it breaks Normalization rules for database design. You have a relation and can already get the results by creating a view or setting CurrentCapacity to a calculated column.
Your view can look like this:
SELECT Id, MaxCapacity, ISNULL(O.SumSize,0) AS CurrentCapacity
FROM dbo.FirstTable FT
OUTER APPLY
(
SELECT ST.FirstTableId, SUM(ST.Size) as SumSize FROM SecTable ST
WHERE ST.FirstTableId = FT.Id
GROUP BY ST.FirstTableId
) O
Sure, you could fire a proc every time a row is updated/inserted or deleted in the second table and recalculate the column, but you might as well calculate it on the fly. If it's not required to have the column accurate, you can have a job update the values every X hours. You could combine this with your view to have both a "live" and "cached" version of the capacity data.

Update column in one table for a user based on count of required records in another table for same user without cursor

I have 2 tables A and B. I need to update a column in table A for all userid's based on the count of records that userid has in another table based on defined rules. If count of records in another table is 3 and is required for that userID, then mark IsCorrect as 1 else 0, if count is 2 and required is 5 then IsCorrect as 0 For e.g. Below is what I am trying to achieve
Table A
UserID | Required | IsCorrect
----------------------------------
1 | SO;GO;PE | 1
2 | SO;GO;PE;PR | 0
3 | SO;GO;PE | 1
Table B
UserID | PPName
-----------------------
1 | SO
1 | GO
1 | PE
2 | SO
2 | GO
3 | SO
3 | GO
3 | PE
I tried using Update in table joining another table, but cannot up with one. Also, do not want to use cursors, because of its overhead. I know I will have to create a stored Procedure for it for the rules, but how to pass the userID's to it without cursor is what am i am looking for.
This is an update for my earlier question. Thanks for the help.
Here's a solution for PostgreSQL:
update TableA
set IsCorrect =
case when
string_to_array(Required, ';') <#
(select array_agg(PPName)
from TableB
where TableA.UserID = TableB.UserID)
then 1
else 0
end;
You can also see it live on SQL Fiddle.
use sub-query and aggregate function and then case when for conditional update
update TableA A
set A.IsCorrect= case when T.cnt>=3 then 1 else 0 end
inner join
(
select B.UserID ,count(*) as cnt from TableB as B
group by UserID
) as T
on A.userid=T.UserID

SQL to search with a subsets of records

Is there any way to select all subsets from table A where corresponding subsets exist in table B? Each subset in table A must have at least all the entries that corresponding subset in table B.
In this link it's called "Division with a Remainder" but my problem is more complex because I've got many to many relation.
Table A
UserName text | File text | AccessLevel int
Table B
AppName text | File text | AccessLevel int
Sample data
Table A
User1 | aaa.txt | 1
User1 | bbb.txt | 3
User1 | ccc.txt | 1
User2 | aaa.txt | 3
Table B
Appl1 | aaa.txt | 1
Appl1 | bbb.txt | 1
Appl2 | aaa.txt | 1
Appl3 | bbb.txt | 5
Appl4 | aaa.txt | 1
Appl4 | bbb.txt | 1
Appl4 | ccc.txt | 1
Appl4 | ddd.txt | 1
Expected results:
User1 | Appl1
User1 | Appl2
User2 | Appl2
User1 has "complete" access to applications Appl1 and Appl2 because he has necessary access to ALL files used by these applications. He doesn't have access to application Appl3 because access level is not high enough. He doesn't have access to application Appl4 because he doesn't have access to file ddd.txt.
Basically I need to compare subsets of records and return all cases where subset in table B is equal or greater than subset in table A. Is there any way to do it in SQL?
Any help appreciated.
One method is a self-join and then comparing the number of matching records to the number needed for the application.
Assuming no duplicates:
select a.username, b.appname
from a join
(select b.*, count(*) over (partition by b.appname) as cnt
from b
) b
on a.filetext = b.filetext and
a.accesslevel >= b.accesslevel
group by a.username, b.appname, b.cnt
having count(*) = b.cnt
SELECT distinct A1.USERNAME, B1.APPLICATION
FROM TABLEUSER A1
INNER JOIN TABLEAPPLI B1 on B1.filename=A1.filename and B1.accesslevel<=A1.accesslevel
where not exists
(
select *
from TABLEUSER A2 INNER JOIN TABLEAPPLI B2 on B2.filename<>A2.filename or B2.accesslevel>A2.accesslevel
where (A1.USERNAME, B1.APPLICATIONAME) = (A2.USERNAME, B2.APPLICATIONAME)
)

Having two SQL Server related tables, select complete rows of one and partial of other

In MS Access was very easy to acomplish but I'm having troubles with SQL Server
I have this query:
SELECT Organigrama.Item, Organigrama.Id, Organigrama.ParentItem, Rol_Menu.Cod_Rol
FROM Rol_Menu RIGHT JOIN
Organigrama ON Rol_Menu.Cod_Menu = Organigrama.Id
WHERE (Rol_Menu.Cod_Rol = '5')
The purpose is to get all the items of Organigrama and the elements in common with Rol_Menu.Col_Rol appears with 5, the others with Null
I need to fill a menu structure into a treeview
When the user select another Rol just get nodes checked that rol have access to
im determining if in the row the Col_Rol isn't null so the query I need to get
something like this:
Item | Id | ParentItem | Cod_Rol
A | 3 | null | 5
B | 4 | A | 5
C | 5 | A | null
D | 6 | B | 5
E | 7 | C | null
F | 8 | E | null
I think you just need to include the extra restriction in the join criteria rather then the where clause. The criteria are evaluated before the outer join adds the null columns. The where clause is evaluated afterwards, and eliminates the nulls.
select
Organigrama.Item,
Organigrama.Id,
Organigrama.ParentItem,
Rol_Menu.Cod_Rol
from
Rol_Menu
right join
Organigrama
on Rol_Menu.Cod_Menu = Organigrama.Id and
Rol_Menu.Cod_Rol = '5'
either that or add or Rol_Menu.Cod_Rol is null to the end of the where clause.

Deleting similar columns in SQL

In PostgreSQL 8.3, let's say I have a table called widgets with the following:
id | type | count
--------------------
1 | A | 21
2 | A | 29
3 | C | 4
4 | B | 1
5 | C | 4
6 | C | 3
7 | B | 14
I want to remove duplicates based upon the type column, leaving only those with the highest count column value in the table. The final data would look like this:
id | type | count
--------------------
2 | A | 29
3 | C | 4 /* `id` for this record might be '5' depending on your query */
7 | B | 14
I feel like I'm close, but I can't seem to wrap my head around a query that works to get rid of the duplicate columns.
count is a sql reserve word so it'll have to be escaped somehow. I can't remember the syntax for doing that in Postgres off the top of my head so I just surrounded it with square braces (change it if that isn't correct). In any case, the following should theoretically work (but I didn't actually test it):
delete from widgets where id not in (
select max(w2.id) from widgets as w2 inner join
(select max(w1.[count]) as [count], type from widgets as w1 group by w1.type) as sq
on sq.[count]=w2.[count] and sq.type=w2.type group by w2.[count]
);
There is a slightly simpler answer than Asaph's, with EXISTS SQL operator :
DELETE FROM widgets AS a
WHERE EXISTS
(SELECT * FROM widgets AS b
WHERE (a.type = b.type AND b.count > a.count)
OR (b.id > a.id AND a.type = b.type AND b.count = a.count))
EXISTS operator returns TRUE if the following SQL statement returns at least one record.
According to your requirements, seems to me that this should work:
DELETE
FROM widgets
WHERE type NOT IN
(
SELECT type, MAX(count)
FROM widgets
GROUP BY type
)