Azure T-SQL SELECT concatenation behavior - sql-server

I have really strange problem (or misunderstanding) of the string concatenation on Azure Sql. I used to build T-SQL code for EXEC like this:
DECLARE #tsql nvarchar(max)
SELECT #tsql = CONCAT(#tsql, Prop1, Prop2) FROM Table
PRINT #tsql
This works fine in SQL from 2012 to 2016. But in Azure SQL I have really strange behavior: only LAST row is in #tsql
Here is live T-SQL to clean up some tables:
DECLARE #deleteTsql nvarchar(max)
SET #deleteTsql = ''
-- just show all table
SELECT O.[name] FROM [sys].[objects] O
LEFT JOIN sys.schemas SCH on O.schema_id = SCH.schema_id
WHERE (SCH.[name] = 'dbo' AND [type] = 'U') or (SCH.[name] = 'data' and O.[name] = 'Objects')
ORDER BY O.create_date desc
-- build T-SQL to cleanup
SELECT #deleteTsql = CONCAT(#deleteTsql, 'PRINT ''Deleting data from ', O.[name], '''', CHAR(10), CHAR(13), 'DELETE FROM [', SCH.[name], '].', '[', O.[name],']', CHAR(10), CHAR(13)) FROM [sys].[objects] O
LEFT JOIN [sys].[schemas] SCH on O.[schema_id] = SCH.[schema_id]
WHERE (SCH.[name] = 'dbo' AND [type] = 'U') or (SCH.[name] = 'data' and O.[name] = 'Objects')
ORDER BY O.create_date desc
PRINT #deleteTsql
This works fine in SQL 2016. For instance I have tables data.Objects, dbo.Table1 and dbo.Table2. Then following will be printed:
(3 rows affected)
PRINT 'Deleting data from Objects'
DELETE FROM [data].[Objects]
PRINT 'Deleting data from Table1'
DELETE FROM [dbo].[Table1]
PRINT 'Deleting data from Table2'
DELETE FROM [dbo].[Table2]
But if I do the same on Azure SQL (v12) database with same tables then I got this:
(3 rows affected)
PRINT 'Deleting data from Table2'
DELETE FROM [dbo].[Table2]
So only last record is in #deleteTsql.
Why?????

The result for aggregate concatenation (with the SELECT #x=#x+... syntax) is undefined. See KB 287515 and this answer. Using CONCAT does not change that. As Panagiotis Kanavos suggested, you should use STRING_AGG or GROUP_CONCAT.
And one more thing: in my databases, I get the results from a single row using your query, even in SQL Server 2016 or SQL Server 2017. So it's not an Azure thing, it's just depending on the execution plan (if it is using a nested loops join, you get all the rows; if it is using a merge join, you get a single row). That may be dependant on the number of rows in the system tables, the CPU count, the updated statistics, etc.

Related

Method to bulk modify triggers in SQL Server database

I have a database that uses Insert, Update, and Delete Triggers for almost all tables. They log the host and program performing the operation in a separate auditing table. The triggers all include this select statement to set variables that get inserted into the auditing table:
select #HostName = HostName, #ProgramName = Program_Name
from master..sysprocesses where SPID = ##SPID
We are now looking to migrate to Azure SQL Database, which does not support the master..sysprocesses syntax. It also appears that table is deprecated as well: https://learn.microsoft.com/en-us/sql/relational-databases/system-compatibility-views/sys-sysprocesses-transact-sql?view=sql-server-ver15
What we need to do is update the triggers to use this instead:
select #HostName = [host_name], #ProgramName = [program_name]
from sys.dm_exec_sessions where session_id = ##SPID
However, the database has hundreds of tables and each table has three triggers that need updating. The text-replacement for each trigger is identical. Is there a feasible way to script out something to perform this update on all triggers in the database?
OK, I just tested this by jamming your string in a few triggers (as a comment of course) and then running it. I am not advocating this as the correct way to do it, as this link will help you with the correct way to do dynamic sql https://dba.stackexchange.com/questions/165149/exec-vs-sp-executesql-performance
However, this does work and will help you understand how you would piece these things together to get to that point.
Note, any formatting difference between your triggers may cause this to miss some, so youll want to verify that 0on your own.
DECLARE #string VARCHAR(8000)='select #HostName = HostName, #ProgramName = Program_Name
from master..sysprocesses where SPID = ##SPID'
, #counter INT=1
, #Max INT
, #Sql VARCHAR(mAX)
;
IF OBJECT_ID('TempDB..#TrigUpdate') IS NOT NULL DROP TABLE #TrigUpdate;
CREATE TABLE #TrigUpdate
(
SqlVar VARCHAR(MAX)
, RowID INT
)
;
INSERT INTO #TrigUpdate
SELECT REPLACE(REPLACE(t.definition, #string, ''), 'CREATE TRIGGER', 'ALTER TRIGGER')
, Row_Number() OVER (ORDER BY t.Definition ASC) AS RowID
FROM sys.objects o
INNER JOIN sys.sql_modules t on o.object_id =t.object_id
WHERE o.type_desc='SQL_TRIGGER'
AND CHARINDEX(#string, t.definition,1)>0
;
SET #Max = (SELECT COUNT(*) FROM #TrigUpdate);
WHILE #Counter<=#Max
BEGIN
SET #sql = (SELECT SqlVar FROM #TrigUpdate WHERE RowID=#counter);
EXEC(#Sql);
SET #Counter=#Counter+1;
END
It could be done with Object_Definition and Replace.
Create Table #Triggers_new (TriggerName sysname, QueryText VarChar(max))
Declare #string_pattern VarChar(max), #string_replacement VarChar(max)
Select #string_pattern = '<string_pattern>'
Select #string_replacement = '<string_replacement>'
Insert Into #Triggers_new (TriggerName, QueryText)
Select [name], Replace(Object_Definition(object_id), #string_pattern, #string_replacement)
From sys.objects
Where [type] = 'TR'
Order by [name]
-- Update #Triggers_new Set QueryText = Replace(QueryText, 'Create Trigger ', 'Alter Trigger ')
Why do you use a so heavy query on system table/view that can be changed without your consent ?
Can't you simplify you by using metada functions like :
SELECT HOST_NAME(), PROGRAM_NAME()...
That will give the requested information values ?

Find a value anywhere in a read-only database

A customer's 96GB SQL Server 2014 accounting database has about 1,000 tables, none of which has constraints or fkeys applied or is otherwise documented. I have read only access, with basically other no rights.
A user has sent me a screenshot showing a value that is stored somewhere in the database. The value is "51210000", which might also be stored as a pointer to its entry in the ACCOUNTS table, 323.
I have seen various solutions to searching an entire db, but they invariably use temporary tables, procedures, or other solutions that require write access. Can anyone offer a way to do this read-only?
It's going to be a bit slow, but you can run these a few at a time to see where it may exist
SELECT CONCAT('select * from ', TABLE_NAME, ' where ', COLUMN_NAME, ' = ''51210000''')
FROM INFORMATION_SCHEMA.COLUMNS
Perhaps sp_MSforeachtable
This is certainly NOT fast and I would suggest testing on a smaller database.
Example
Declare #Results table (TableName varchar(500),RowData varchar(max))
Insert Into #Results
EXEC sp_MSforeachtable 'SELECT TableName=''?'' ,RowData = (Select A.* for XML Raw) FROM ? A Where (Select A.* for XML Raw) like ''%51210000%'''
Select *
From #Results
Note:
If 2016+, you may get a little boost by using the JSON alternative.
Replace (Select A.* for XML Raw)
With (Select A.* for JSON Path)
Just for fun
I ran a test looking for "Consulting" on a 17GB database (214 tables). It took 1 minute 30 seconds to return
EDIT - Dynamic SQL Approach
Declare #SQL varchar(max)
Set #SQL=Stuff((Select 'Union All ' +Expr
From (
Select Expr = 'Select Table_Schema='''+Table_Schema+''',Table_Name='''+Table_Name+''',Column_Name='''+Column_Name+''',Value=cast('+quotename(Column_Name)+' as varchar(max)) From '+quotename(Table_Schema)+'.'+quoteName(Table_Name)+' Where '+quotename(Column_Name)+' like ''%Cappe%''||'
From INFORMATION_SCHEMA.COLUMNS
Where Data_Type in ('varchar','int','float','bigint') -- << Set Your Desired Filter
and Table_Name not like 'vw_%' -- << I'd tend to exclude views my prefix vw_
) A
For XML Path ('')),1,10,'')
Set #SQL = replace(#SQL,'||',char(13))
Print #SQL
Exec(#SQL)
Returns

How can I select this in Microsoft Query?

I have a stored procedure and a simple table.
I need to join this two object then allow user to see the result using Microsoft Query in Excel.
This is what I have. The Exec SP_Budget create global temp table and fill ##tmpBudget
exec SP_Budget;
Select g.name, g.address,g.Amount,b.BudgetAmt from gTable g
Left join ##tmpBudget b on b.NameID=g.NameID
Microsoft query can only do a simple
select * from tTable
how can I do this?
I have to have the stored procedure because it does UNpivot inside the SP_Budget
EDIT:
The more I think about this. I think I need to post the original SP_Budget
so here it is. Because maybe a better approach is to create a function rather than SP_Budget. Here is my SP_Budget. Is it possible to convert this SP to a function?
--this link show me how to build column list for the PIVOT/UNPIVOT http://stackoverflow.com/questions/9585094/sql-server-pivots-displaying-row-values-to-column-headers
--this link show me how to build column list of a specific table http://stackoverflow.com/questions/18775409/unpivot-with-dynamic-columns-plus-column-names
--here we build the dynamic query for the column list for the PIVOT/UNPIVOT
declare #sql AS NVARCHAR(MAX);
declare #cols nvarchar(max);
select #cols = coalesce(#cols+N',', N'') + quotename(c.name) from syscolumns c
inner join sysobjects o on c.id = o.id and o.xtype = 'u'
where o.name = 'AcctHist' and c.name not in ('PID', 'UID') and c.name like 'ptdbal%' and c.name <> 'PtdBal12' order by c.colid
--Construct the full T-SQL statement
--and execute dynamically
SET #sql = N'select
DB,Acct,Sub,cpnyid
,Fiscyr+
CASE WHEN len(Convert(varchar(2),CONVERT(int,right(Period,2))+1))=1
THEN ''0''+Convert(varchar(2),CONVERT(int,right(Period,2))+1)
ELSE Convert(varchar(2),CONVERT(int,right(Period,2))+1)
END "Period"
,Amounts
from (
SELECT A.DB,A.Sub,A.acct,A.FiscYr,A.CpnyID
, sum(A.PtdBal00) "PtdBal00"
,sum(A.PtdBal01) "PtdBal01"
,sum(A.PtdBal02) "PtdBal02"
,sum(A.PtdBal03) "PtdBal03"
,sum(A.PtdBal04) "PtdBal04"
,sum(A.PtdBal05) "PtdBal05"
,sum(A.PtdBal06) "PtdBal06"
,sum(A.PtdBal07) "PtdBal07"
,sum(A.PtdBal08) "PtdBal08"
,sum(A.PtdBal09) "PtdBal09"
,sum(A.PtdBal10) "PtdBal10"
,sum(A.PtdBal11) "PtdBal11"
FROM vz_Finance_Budget A
Group by A.DB,A.Sub,A.acct,A.FiscYr,A.CpnyID
)xx
UNPIVOT (Amounts for Period in ('+#cols+')) unpiv;';
EXEC sp_executesql #sql;
Edit2:
Thank you for all the suggestions. I decided that the limiting factor is microsoft query itself. Plus microsoft query should be replaced with something newer (I think). Instead of configuring my server to allow openrowset or OPENQuery. I will look into microsoft query replacement.
not sure if this is the right way to go. but I will update here. thanks.
Edit3: Trying this blog now: http://blogs.office.com/2010/06/07/running-a-sql-stored-procedure-from-excel-no-vba/ I will update when I complete it
As far as I know you can have explicit JOIN in Microsoft Query
Description of the usage of joins in Microsoft Query
SELECT ....
FROM tbl1, tbl2
WHERE tbl1.Id = tbl2.Id
Update
If you want to have the result set in Excel, You can use OPENROWSET to export the data from SQL Server to Excel.
INSERT INTO OPENROWSET('Microsoft.Jet.OLEDB.4.0',
'Excel 8.0;Database=C:\testing.xls;',
'SELECT name, address, Amount, BudgetAmt FROM [Sheet1$]')
SELECT g.name, g.address, g.Amount, b.BudgetAmt FROM gTable g
LEFT JOIN #tmpBudget b ON b.NameID = g.NameID
As you just mentioned earlier, you need to save your proc result into a temp table first
sp_configure 'Show Advanced Options', 1
GO
RECONFIGURE
GO
sp_configure 'Ad Hoc Distributed Queries', 1
GO
RECONFIGURE
GO
SELECT * INTO #tmpBudget
FROM OPENROWSET('SQLNCLI'
,'Server=(local)\SQL2008;Trusted_Connection=yes;'
,'EXEC SP_Budget')
I solved this using this blog below
http://blogs.office.com/2010/06/07/running-a-sql-stored-procedure-from-excel-no-vba/
basically in excel, when you click data tab - From other sources - instead of "from microsoft query". I select "from SQL server". With "from SQL Server". I have the option in the "Connection properties - definition" to select the command type to SQL (instead of table)
the blog explains this with screenshot... Thanks

Join queries from sp_MSForEachTable

I'm using the following query to get the times when a table was last updadted by users:
EXEC sp_MSForEachTable 'SELECT ''?'' as TableName,
last_user_update,
user_updates,
index_id
FROM sys.dm_db_index_usage_stats
WHERE database_id = DB_ID(''SP3D_DB_RESEARCH_MDB'') AND
OBJECT_ID = OBJECT_ID(''?'')'
But in the results window I get like a table with one row for each result. Is there any way to join them as an unique query so I can sort the results?
I'm using SQL Server 2008 R2 and Microsoft SQL Server Management Studio.
You can not JOIN with the procedure but you will be able to get the result into one (temp)Table(variable), which you might use for joins in a second step using INSERT INTO with the EXEC. e.g.
Declare #Collect Table (Name Varchar(100),cnt integer)
Insert into #Collect
EXEC sp_MSForEachTable 'SELECT ''?'' as TableName,Count(*) from ?'
Select * from #Collect

SQL Server 2005: SELECT default value of columns

I'm writing a script that analyses database tables and I'm having problems with fetching the default value of columns from SQL Server 2005.
The following works fine on SQL Server 2008, but when I run the same thing on SQL Server 2005, the result is an empty string, and I can't figure out why.
DECLARE #sDatabase VARCHAR(100);
DECLARE #sTable VARCHAR(100);
DECLARE #sColumn VARCHAR(100);
SET #sDatabase = 'SomeDatabase';
SET #sTable = 'tbl_something';
SET #sColumn = 'SomethingID';
SELECT
syscolumns.name AS column_name,
systypes.name column_type,
syscolumns.length AS column_length,
syscolumns.prec AS column_precision,
syscolumns.scale AS column_scale,
syscolumns.isnullable AS column_nullable,
syscomments.text AS column_default_value -- Should be default value!
FROM
sysobjects
INNER JOIN syscolumns ON sysobjects.id = syscolumns.id
INNER JOIN systypes ON syscolumns.xtype = systypes.xtype
LEFT OUTER JOIN syscomments ON syscomments.id = syscolumns.cdefault
WHERE sysobjects.xtype = 'U'
AND sysobjects.name = #sTable
AND systypes.name != 'sysname'
ORDER BY
sysobjects.name,
syscolumns.colid
Another version is as follows, but it's the same story; returns an empty string on SQL Server 2005, but the correct value on SQL Server 2008.
SELECT COLUMN_DEFAULT
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_CATALOG = #sDatabase
AND TABLE_NAME = #sTable
AND COLUMN_NAME = #sColumn
This is used for comparing certain changes between databases. Originally I thought that the problem was because SQL Server 2005 returned default values such as "((0))" while SQL Server 2008 was returning "(0)" for the same default value of the integer 0. That's what SQL Server Management Studio reports respectively. But upon further investigation, it turns out that the default values of SQL Server 2005 are simply not being displayed at all, so it is always recorded as a change when in fact there is none.
Any help deeply appreciated.
Thanks!
Have you tried the sys.default_constraints view?
This snippet finds the default for table TestTable column nr:
create table TestTable (nr float default 3.13)
select definition
from sys.default_constraints dc
join sys.columns c
on c.object_id = dc.parent_object_id
and c.column_id = dc.parent_column_id
where parent_object_id = object_id('TestTable')
and c.name = 'nr'

Resources