Query with columns calculated at runtime - sql-server

I need some help with a query in SQL Server 2012.
My customer has a (sort of) internal wiki where his employees save documents; these documents are stored in a table in the database (let's call it Documentation, with columns ID, Title, Text and Tags) and accessed via a web frontend.
Up to now every employee had access to all the documents, now my customer now wants his employees to access only to specific procedures. There are no specific criteria such as role or user type, he wants to decide who sees what on an individual basis (from now on John will see documents 1,2 and 4 while Janet will see only documents 3,4 and 5 and so on). Don't ask me why...
My task is to prepare a big table in the frontend where every line is a document and the employees are in the column; for every document there is a checkbox for every employee, to indicate whether that user can access the document, something similar to this:
The problem is that the number of documents is not fixed, nor the number of employees. I need to find a query to extract this data without knowing the number of columns, it must be done dynamically at runtime.
I have access to the User Table of course, so I know the number and the name of the employees. As for the authorizations I thought I could use a new table with just 3 columns: ID, ID_Document and ID_User.
I googled and searched but I could not find an appropriate answer. I tried using a Pivot Table but it does not seem right to me, I do not have to do any aggregation to the data.
Can anybody help me?

I'm going to we assume you have 5 tables in total. I'm going to call these Document, Tag, Employee and then DocumentTag and DocumentEmployee. To therefore get the solution you are after you need 2 different types of aggregation, string aggregation for the tags, and pivoting for the employees.
--Base tables
CREATE TABLE dbo.Document (DocumentID int, DocumentTitle nvarchar(50));
CREATE TABLE dbo.Employee (EmployeeID int, EmployeeName nvarchar(50));
CREATE TABLE dbo.Tag (TagID int, TagName nvarchar(50));
GO
--Link tables
CREATE TABLE dbo.DocumentTag (DocumentID int, TagID int);
CREATE TABLE dbo.DocumentEmployee (DocumentID int, EmployeeID int);
GO
--Sample data
INSERT INTO dbo.Document
VALUES(2,N'Important Doc'),(3,N'New Doc');
INSERT INTO dbo.Employee
VALUES(1,N'John'),
(2,N'Mary'),
(3,N'Patricia'),
(4,N'Paul');
INSERT INTO dbo.Tag
VALUES(1,N'Classified'),
(2,N'Finance'),
(3,N'Warehouse');
GO
--Link Data
INSERT INTO dbo.DocumentTag
VALUES(1,1),(1,2),(2,3);
INSERT INTO dbo.DocumentEmployee
VALUES(1,1),(1,2),(1,3),(2,2),(2,4);
If didn't need a dynamic pivot, then your SQL would look somethuing like this:
SELECT D.DocumentID,
D.DocumentTitle,
STUFF((SELECT N' ' + T.TagName
FROM dbo.DocumentTag DT
JOIN dbo.Tag T ON DT.TagID = T.TagID
WHERE DT.DocumentID = D.DocumentID
FOR XML PATH(N''),TYPE).value('.','nvarchar(MAX)'),1,1,N'') AS Tags,
MAX(CASE E.EmployeeName WHEN N'John' THEN 1 ELSE 0 END) AS John,
MAX(CASE E.EmployeeName WHEN N'Mary' THEN 1 ELSE 0 END) AS Mary,
MAX(CASE E.EmployeeName WHEN N'Patricia' THEN 1 ELSE 0 END) AS Patricia,
MAX(CASE E.EmployeeName WHEN N'Paul' THEN 1 ELSE 0 END) AS Paul
FROM dbo.Document D
JOIN dbo.DocumentEmployee DE ON D.DocumentID = DE.EmployeeID
JOIN dbo.Employee E ON DE.EmployeeID = E.EmployeeID
GROUP BY D.DocumentID,
D.DocumentTitle;
As you need the dataset to scale as you add employees, however, you need dynamic SQL to do this. So to achieve this using the above solution you can do something like this:
DECLARE #SQL nvarchar(MAX),
#CRLF nchar(2) = NCHAR(13) + NCHAR(10);
SET #SQL = N'SELECT D.DocumentID,' + #CRLF +
N' D.DocumentTitle,' + #CRLF +
N' STUFF((SELECT N'' '' + T.TagName' + #CRLF +
N' FROM dbo.DocumentTag DT' + #CRLF +
N' JOIN dbo.Tag T ON DT.TagID = T.TagID' + #CRLF +
N' WHERE DT.DocumentID = D.DocumentID' + #CRLF +
N' FOR XML PATH(N''''),TYPE).value(''.'',''nvarchar(MAX)''),1,1,N'''') AS Tags,' + #CRLF +
STUFF((SELECT N',' + #CRLF +
N' MAX(CASE E.EmployeeName WHEN N' + QUOTENAME(E.EmployeeName,'''') + N' THEN 1 ELSE 0 END) AS ' + QUOTENAME(E.EmployeeName)
FROM dbo.Employee E
ORDER BY E.EmployeeID ASC
FOR XML PATH(''),TYPE).value('.','nvarchar(MAX)'),1,3,N'') + #CRLF +
N'FROM dbo.Document D' + #CRLF +
N' JOIN dbo.DocumentEmployee DE ON D.DocumentID = DE.EmployeeID' + #CRLF +
N' JOIN dbo.Employee E ON DE.EmployeeID = E.EmployeeID' + #CRLF +
N'GROUP BY D.DocumentID,' + #CRLF +
N' D.DocumentTitle;';
--PRINT #SQL; --YOur debugging friend
EXEC sys.sp_executesql #SQL;
DB<>Fiddle

Related

How to search for a value in a common column across multiple tables in a SQL Server database?

I have almost 1000 tables and most of them have a common column ItemNumber. How do I search across all the tables in the database for a value or list of values that exist in this common column, such as 350 or (350, 465)? The tables have different schemas.
Table A100
ItemNumber
Detail
230
Car
245
Plane
Table A1000
ItemNumber
ProductDescription
350
Pie
465
Cherry
This does not perform type checking, so you can get conversion errors if the target column is not the correct type. Also, this script uses LIKE, you would probably need to change that to a direct comparison.
SET NOCOUNT ON
DECLARE #ID NVARCHAR(100) = '2'
DECLARE #ColumnName NVARCHAR(100) ='UserID'
DECLARE #Sql NVARCHAR(MAX)=N'CREATE TABLE #TempResults(TableName NVARCHAR(50), ColumnName NVARCHAR(50), ItemCount INT)'
SELECT
#Sql = #Sql + N'INSERT INTO #TempResults SELECT * FROM (SELECT '''+ST.Name+''' AS TableName, '''+C.Name+''' AS ColumnName, COUNT(*) AS ItemCount FROM '+ST.Name+' WHERE '+C.Name+'='+#ID+') AS X WHERE ItemCount > 0 '
FROM
sys.columns C
INNER JOIN sys.tables ST ON C.object_id = ST.object_id
WHERE
C.Name LIKE '%'+#ColumnName+'%'
SET #Sql = #Sql + N'SELECT * FROM #TempResults'
exec sp_executesql #sql
You need to do this with dynamic SQL. You will need to query all 1000 tables, and make sure you are converting the values correctly if the columsn are different types.
You don't need a temp table for this, you can just script one giant UNION ALL query. You must make sure to quote all dynamic names correctly using QUOTENAME.
To be able to return data for multiple items, you should create a Table Valued Parameter, which you can pass in using sp_executesql.
First create a table type
CREATE TYPE dbo.IntList (Id int PRIMARY KEY);
Then you create a table variable containing them, and pass it in. You can also do this in a client application and pass in a TVP.
SET NOCOUNT ON;
DECLARE #Items dbo.IntList;
INSERT #Items (Id) VALUES(350),(465);
DECLARE #Sql nvarchar(max);
SELECT
#Sql = STRING_AGG(CONVERT(nvarchar(max), N'
SELECT
' + QUOTENAME(t.name, '''') + ' AS TableName,
t.ItemNumber,
COUNT(*) AS ItemCount
FROM ' + QUOTENAME(t.Name) + ' t
JOIN #items i ON i.Id = t.ItemNumber
GROUP BY
t.ItemNumber
HAVING COUNT(*) > 0
' ),
N'
UNION ALL
' )
FROM
sys.tables t
WHERE t.object_id IN (
SELECT c.object_id
FROM sys.columns c
WHERE
c.Name = 'ItemNumber'
);
PRINT #sql; -- your friend
EXEC sp_executesql
#sql,
N'#items dbo.IntList',
#items = #items READONLY;
If you don't need to know the count, and only want to know if a value exists, you can change the dynamic SQL to an EXISTS
....
SELECT
#Sql = STRING_AGG(CONVERT(nvarchar(max), N'
SELECT
' + QUOTENAME(t.name, '''') + ' AS TableName,
t.ItemNumber
FROM #items i
WHERE i.Id IN (
SELECT t.ItemNumber
FROM ' + QUOTENAME(t.Name) + ' t
)
' ),
N'
UNION ALL
' )
....

Why FOR XML PATH() is used in this script?

I got a help from some one to compose below sql but full script is not written by me. so i have bit of confusion how the below sql is working ?
CREATE Proc USP_GetValuationValue
(
#Ticker VARCHAR(10),
#ClientCode VARCHAR(10),
#GroupName VARCHAR(10)
)
AS
DECLARE #SPID VARCHAR(MAX), --Is this even used now?
#SQL nvarchar(MAX),
#CRLF nchar(2) = NCHAR(13) + NCHAR(10);
SELECT #SPID=CAST(##SPID AS VARCHAR);
SET #SQL = N'SELECT * FROM (SELECT min(id) ID,f.ticker,f.ClientCode,f.GroupName,f.RecOrder,' + STUFF((SELECT N',' + #CRLF + N' ' +
N'MAX(CASE FieldName WHEN ' + QUOTENAME(FieldName,'''') + N' THEN FieldValue END) AS ' + QUOTENAME(FieldName)
FROM tblValuationSubGroup g
WHERE ticker=#Ticker AND ClientCode=#ClientCode AND GroupName=#GroupName
GROUP BY FieldName
ORDER BY MIN(FieldOrder)
FOR XML PATH(''),TYPE).value('(./text())[1]','nvarchar(MAX)'),1,10,N'') + #CRLF +
N'FROM (select * from tblValuationFieldValue' + #CRLF +
N'WHERE Ticker = #Ticker AND ClientCode = #ClientCode AND GroupName= #GroupName) f' + #CRLF +
N'GROUP BY f.ticker,f.ClientCode,f.GroupName,f.RecOrder) X' + #CRLF +
N'ORDER BY Broker;';
PRINT #SQL;
Below sql is generated after executing above dynamic sql
SELECT * FROM (SELECT min(id) ID,f.ticker,f.ClientCode,f.GroupName,f.RecOrder,
MAX(CASE FieldName WHEN 'Last Update' THEN FieldValue END) AS [Last Update],
MAX(CASE FieldName WHEN 'Broker' THEN FieldValue END) AS [Broker],
MAX(CASE FieldName WHEN 'Rating' THEN FieldValue END) AS [Rating],
MAX(CASE FieldName WHEN 'Equivalent Rating' THEN FieldValue END) AS [Equivalent Rating],
MAX(CASE FieldName WHEN 'Target Price' THEN FieldValue END) AS [Target Price]
FROM (select * from tblValuationFieldValue
WHERE Ticker = #Ticker AND ClientCode = #ClientCode AND GroupName= #GroupName) f
GROUP BY f.ticker,f.ClientCode,f.GroupName,f.RecOrder) X
ORDER BY Broker;
This part is not clear why used in above sql?
FOR XML PATH(''),TYPE).value('(./text())[1]','nvarchar(MAX)'),1,10,N'')
why FOR XML PATH() has been used here ? i always use FOR XML PATH() to generate xml with data from table.
please help me to understand first dynamic sql like how it is working.
Thanks
FOR XML PATH('') is used in older versions of SQL server (pre 2017) to get values from multiple rows into one. Since you're setting a variable, #SQL, you're setting a single string and therefore need one row returned.
FOR XML PATH('') produces all the MAX(CASE FieldName [...] with data from the tblValuationSubGroup table. Each row in tblValuationSubGroup contains one field. FOR XML PATH('') adds the rows into one string, which is later turned into fields during execution.
The issue FOR XML PATH('') solves in older versions of SQL Server has been solved with the STRING_AGG() function since SQL Server 2017.
https://learn.microsoft.com/en-us/sql/t-sql/functions/string-agg-transact-sql?view=sql-server-ver16
FOR XML PATH is being to dynamically generate the last 5 columns in your query (the MAX(CASE FieldName WHEN '...' THEN FieldValue END) AS [...] columns) using the FieldName column of tblValuationSubGroup.

SQL Server need to find all records for same user across multiple tables

I have a list of tables and a list of users. I need to find all of the occurrences of each user that happen to occur in every table in that is in the list. I need to search each table for one user ID at a time. I have 310,000 users to search for and 400 tables to search through. Each User needs to be searched for on each table. I'm not quite sure about the best way to go about this as I don't know how I could loop through either list to find all the records for each user.
Summary:
310,000 users
400 tables
need to find how many records each user has in every table.
This is a complete stab in the dark, but this might be what you are after. This assumes you are using a fully supported version of SQL Server (as you haven't stated otherwise); if not you'll need to use the old FOR XML PATH (and STUFF) method for string aggregation.
DECLARE #ColumnName sysname = N'YourColumnName',
#CRLF nchar(2) = NCHAR(13) + NCHAR(10),
#SQL nvarchar(MAX);
SELECT #SQL = STRING_AGG(N'SELECT N' + QUOTENAME(t.[name],'''') + N' AS TableName,' + #CRLF +
N' ' + QUOTENAME(c.name) + N',' +#CRLF +
N' COUNT(*) AS RowsInTable' + #CRLF +
N'FROM ' + QUOTENAME(s.[name]) + N'.' + QUOTENAME(t.name) + N';',#CRLF)
FROM sys.schemas s
JOIN sys.tables t ON s.schema_id = t.schema_id
JOIN sys.columns c ON t.object_id = c.object_id
WHERE t.[name] IN (SELECT YT.TableName
FROM dbo.YourTableOfTables YT)
AND c.[name] = #ColumnName;
CREATE TABLE #UserRowCounts (TableName sysname,
UserID int, --Guessed data type
RowsInTable int);
INSERT INTO #UserRowCounts
EXEC sys.sp_executesql #SQL;
SELECT TableName,
UserID,
RowsInTable
FROM #UserRowCounts URC
WHERE URC.UserID IN (SELECT YT.UserID
FROM dbo.YourTableOfUsers)
ORDER BY UserID,
TableName;
As mentioned in the comments, this probably won't be quick; but then you are counting the rows from 400~ tables in a single batch so you shouldn't expect it to be.

SELECT One column from multiple tables MSSQL

Easy task is giving me a hard time. I want to select one column from different tables and insert it in a results table. Basically, much like a union all:
SELECT Email FROM TableA
UNION ALL
Select Email FROM TableB
and so on...
However, I want to do this in an automated way. As I said, seems so simple, but I am stumbling over it. My code attempt:
USE MyDatabase
IF OBJECT_ID ('TEMPDB..#Selection') IS NOT NULL DROP TableA #Selection;
SELECT Name AS TableA, ROW_NUMBER() OVER ( ORDER BY (SELECT 1)) AS RowNumb
INTO #Selection
FROM Sys.TableAs AS T
WHERE NAME LIKE '%abc%'
ORDER BY 2
IF OBJECT_ID ('TEMPDB..#Result') IS NOT NULL DROP TableA #Result;
CREATE TableA #Result ( Email VARCHAR (200))
DECLARE #Counter INT
SET #Counter = 1
WHILE #Counter <= ( SELECT MAX (RowNumb) FROM #Selection )
BEGIN
DECLARE #Table VARCHAR (100)
SET #Table = ( SELECT TableA FROM #Selection WHERE RowNumb = #Counter )
-- PRINT #Table SET #Counter = #Counter + 1 END
INSERT INTO #Result
SELECT Email
FROM #Table
SET #Counter = #Counter + 1
END
I am sure someone will find my mistakes quickly. Thanks a lot for any guidance!
Kind regards, M.
I would (personally) go for something more like this:
CREATE TABLE #Email (email nvarchar(200));
DECLARE #SQL nvarchar(MAX);
SET #SQL = N'INSERT INTO #Email (Email)' + NCHAR(13) + NCHAR(10) +
STUFF((SELECT NCHAR(13) + NCHAR(10) +
N'UNION ALL' + NCHAR(13) + NCHAR(10) +
N'SELECT CONVERT(nvarchar(200),email)' + NCHAR(13) + NCHAR(10) +
N'FROM ' + QUOTENAME(s.[name]) + N'.' + QUOTENAME(t.[name])
FROM sys.schemas s
JOIN sys.tables t ON s.schema_id = t.schema_id
JOIN sys.columns c ON t.object_id = c.object_id
WHERE c.[name] = N'Email'
FOR XML PATH(N''),TYPE).value(N'.','nvarchar(MAX)'),1,13,N'') + N';';
PRINT #SQL;
EXEC sp_executesql #SQL;
SELECT *
FROM #Email;
DROP TABLE #Email;
This creates a dynamic statement that creates a UNION ALL query against every table with the column Email (in the current database) and inserts the value into said temporary table. It then returns said values from the temporary table (and then disposes of the table, as I don't actually know what you're going to do with it).

Get number of tables in each database in SQL Server

SELECT d.NAME
,ROUND(SUM(mf.size) * 8 / 1024, 0) Size_MBs
,(SUM(mf.size) * 8 / 1024) / 1024 AS Size_GBs
FROM sys.master_files mf
INNER JOIN sys.databases d ON d.database_id = mf.database_id
WHERE d.database_id > 4
GROUP BY d.NAME
ORDER BY d.NAME
I have the above T-SQL script which lists all databases on an SQL Server instance along with their corresponding size in MBs & GBs.
What i'm struggling with is also to include a column for the number of tables in each database.
Does any one also know how i can improve the above script to also show the total numbers of tables in each listed database. Optionally, it would be nice to get also the number of rows in each table but this is not a big issue.
I'm targeting sql server 2005 and obove.
Not as a single script, you could obtain the requested result by
CREATE TABLE AllTables ([DB Name] sysname, [Schema Name] sysname, [Table Name] sysname)
DECLARE #SQL NVARCHAR(MAX)
SELECT #SQL = COALESCE(#SQL,'') + '
insert into AllTables
select ' + QUOTENAME(name,'''') + ' as [DB Name], [Table_Schema] as
[Table Schema], [Table_Name] as [Table Name] from ' +
QUOTENAME(Name) + '.INFORMATION_SCHEMA.Tables;' FROM sys.databases
ORDER BY name
EXECUTE(#SQL)
SELECT * FROM AllTables ORDER BY [DB Name],[SCHEMA NAME], [Table Name]
DROP TABLE AllTables
Reference:List of All Tables in All Databases
A wider report about all your databases could include stats over tables, views, procedures, triggers, amenities (xml, spatial indexes) and so far.
if object_ID('TempDB..#AllTables','U') IS NOT NULL drop table #AllTables
CREATE TABLE #AllTables (
[DB Name] sysname,
[Tables] int,
[Views] int,
[Procedures] int,
[Triggers] int,
[Full Text Catalogs] int,
[Xml Indexes] int,
[Spatial Indexes] int)
DECLARE #SQL NVARCHAR(MAX)
SELECT #SQL = COALESCE(#SQL,'') + 'USE ' + quotename(name) + '
insert into #AllTables
select ' + QUOTENAME(name,'''') + ' as [DB Name],
(select count(*) from ' + QUOTENAME(Name) + '.sys.tables),
(select count(*) from ' + QUOTENAME(Name) + '.sys.views),
(select count(*) from ' + QUOTENAME(Name) + '.sys.procedures),
(select count(*) from ' + QUOTENAME(Name) + '.sys.triggers),
(select count(*) from ' + QUOTENAME(Name) + '.sys.fulltext_catalogs),
(select count(*) from ' + QUOTENAME(Name) + '.sys.xml_indexes),
(select count(*) from ' + QUOTENAME(Name) + '.sys.spatial_indexes)
'
FROM sys.databases
ORDER BY name
-- print #SQL -- debug
EXECUTE(#SQL)
SELECT * FROM #AllTables
The provided solution, being far to be optimal, is easy to master and extend.
NOT OPTIMAL, just for sporadic report
based on (and credits to): http://blogs.lessthandot.com/index.php/DataMgmt/DataDesign/how-to-get-information-about-all-databas/

Resources