-1

My purpose is to produce a table containing the table_name, column_name, number of row each column and number of null value in each column. But I get an error:

Conversion failed when converting the varchar value ', ' to data type int

These are my queries:

DECLARE @BANG TABLE 
              (
                  TABLE_NAME NVARCHAR(MAX), 
                  COLUMN_NAME NVARCHAR(MAX), 
                  ID INT IDENTITY(1, 1)
              )

INSERT INTO @BANG (TABLE_NAME, COLUMN_NAME)
    SELECT A.NAME AS TABLE_NAME, B.NAME AS COLUMN_NAME 
    FROM SYS.TABLES AS A
    LEFT JOIN SYS.COLUMNS AS B ON A.OBJECT_ID = B.OBJECT_ID
    WHERE 1=1 
      AND A.NAME IN ('CTHD', 'HOADON', 'SANPHAM', 'KHACHHANG', 'NHANVIEN')

DECLARE @RESULT TABLE 
                (
                    TABLE_NAME NVARCHAR(MAX), 
                    COLUMN_NAME NVARCHAR(MAX), 
                    TOTAL_ROW INT, 
                    TOTAL_NULL INT
                )

DECLARE @ID INT = 0

WHILE @ID <= (SELECT COUNT(*) FROM @BANG)
BEGIN
    DECLARE @TABLE_NAME NVARCHAR(MAX)
    SET @TABLE_NAME = (SELECT TABLE_NAME FROM @BANG WHERE @ID = ID)

    DECLARE @COLUMN_NAME NVARCHAR(MAX)
    SET @COLUMN_NAME = (SELECT COLUMN_NAME FROM @BANG WHERE ID = @ID)

    DECLARE @TOTAL_ROW INT
    DECLARE @TOTAL_NULL INT

    DECLARE @SQL NVARCHAR(MAX)
    SET @SQL = 'SET @TOTAL_ROW = (SELECT COUNT(*) FROM '+@TABLE_NAME+')
                SET @TOTAL_NULL = (SELECT COUNT(*) FROM '+@TABLE_NAME+' WHERE '+@COLUMN_NAME+' IS NULL)
                INSERT INTO @RESULT
                VALUES ('+@TABLE_NAME+', '+@COLUMN_NAME+', '+@TOTAL_ROW+', '+@TOTAL_NULL+')
                '

    SET @ID += 1

    EXEC (@SQL)
END

I need your help. Thanks in advance

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
  • You know that SQL doesn't have to all be in capitals, right? – Thom A Mar 21 '21 at 14:44
  • Does this answer your question? [Single SQL query to find null values in all columns in a data base](https://stackoverflow.com/q/60186884/2029983) – Thom A Mar 21 '21 at 14:51
  • Don't re-invent the wheel. Learn to search the internet before post a question. You can find many examples of counting rows in all (or some subset) of tables - like [this](https://stackoverflow.com/questions/16528682/count-null-values-from-multiple-columns-with-sql/16528738) – SMor Mar 21 '21 at 15:30
  • in my case, the output should be a table containing the table_name; column_name; total_row of column; total_null of column using dynamic objects. I tried but failed to complete. So I asked. – Dustin Nguyen Mar 21 '21 at 16:45

3 Answers3

1

You should be using parameterized SQL. But honestly, the code is such a mess that I'm not going to attempt that fix.

The problem is that parameters such as @TOTAL_ROW are integers, not strings. So, the + is treated as addition rather than string concatenation.

The simplest immediate fix is to use CONCAT():

  SET @SQL = CONCAT('
INSERT INTO @RESULT
    VALUES (''', @TABLE_NAME, ''', ''', @COLUMN_NAME, ''', ''', @TOTAL_ROW, ', ', @TOTAL_NULL, ')';

You may have the same error elsewhere in the code. You need to fix all places where you have a number and string combined with + and you intend string concatenation rather than addition.

However, the real fix is to not munge query strings with such values. Instead use sp_executesql passing the values in as parameters.

Gordon Linoff
  • 1,242,037
  • 58
  • 646
  • 786
  • use sp_executesql produces the same error. and using concat() can you show me a complete-sample query? thanks – Dustin Nguyen Mar 21 '21 at 14:27
  • My personal suggestion would be that this doesn't properly quote the dynamic objects, thus open to injection or syntax error when dealing with poorly named columns. – Thom A Mar 21 '21 at 14:52
  • Plus, changing to `sp_executesql` when the values are *still* injected literally solves nothing. `EXEC (@SQL)` and `EXEC sys.sp_executesql @SQL;` are just as bad as each other if the dynamic SQL is poorly written. `sys.sp_executesql` doesn't stop injection, but it does allow for parametrisation. If you don't bother parametrising, then it's pointless recommending `sys.sp_executesql`. – Thom A Mar 21 '21 at 14:58
  • Actually using dynamic objects is the requirement. by the way, can you show me another better approach for this problem? Thanks – Dustin Nguyen Mar 21 '21 at 15:10
1

The conversion error is during the generation of the dynamic SQL query, not during execution of the statement.

There are a number of issues with the script in your question. Below is a script that uses QUOTENAME to more security build the SQL statement and uses a parameterized query to execute it. The WHILE pseudo cursor doesn't provide any value in this case so this version uses a real cursor.

DECLARE @RESULT TABLE (SCHEMA_NAME sysname, TABLE_NAME sysname, COLUMN_NAME sysname, TOTAL_ROW int, TOTAL_NULL int);
DECLARE @SQL nvarchar(MAX), @SchemaName sysname, @TableName sysname, @ColumnName sysname;
DECLARE BANG CURSOR LOCAL FAST_FORWARD FOR
    SELECT s.name AS SCHEMA_NAME, t.name AS TABLE_NAME, c.name AS COLUMN_NAME
    FROM sys.tables AS t
    JOIN sys.schemas AS s ON s.schema_id = t.schema_id
    JOIN sys.columns AS c ON c.object_id = t.object_id
    WHERE t.name IN (N'CTHD', N'HOADON', N'SANPHAM', N'KHACHHANG', N'NHANVIEN');
OPEN BANG;
WHILE 1 = 1
BEGIN
    FETCH NEXT FROM BANG INTO @SchemaName, @TableName, @ColumnName;
    IF @@FETCH_STATUS = -1 BREAK;
    SET @SQL = N'SELECT @SchemaName, @TableName, @ColumnName, COUNT(*), COALESCE(SUM(CASE WHEN ' + QUOTENAME(@ColumnName) + N' IS NULL THEN 1 ELSE 0 END),0)
    FROM ' + QUOTENAME(@SchemaName) + N'.' + QUOTENAME(@TableName) + N';'
    PRINT @SQL
    INSERT INTO @RESULT(SCHEMA_NAME, TABLE_NAME, COLUMN_NAME, TOTAL_ROW, TOTAL_NULL)
        EXEC sp_executesql @SQL
            , N'@SchemaName sysname, @TableName sysname, @ColumnName sysname'
            , @SchemaName = @SchemaName
            , @TableName = @TableName
            , @ColumnName = @ColumnName;
END;
CLOSE BANG;
DEALLOCATE BANG;
SELECT SCHEMA_NAME, TABLE_NAME, COLUMN_NAME, TOTAL_ROW, TOTAL_NULL
FROM @RESULT
ORDER BY SCHEMA_NAME, TABLE_NAME, COLUMN_NAME;
GO

If you don't have many tables/columns, you could use a single UNION ALL query and ditch the (pseudo)cursor entirely:

DECLARE @SQL nvarchar(MAX) = (SELECT STRING_AGG(
      N'SELECT ' + QUOTENAME(s.name,'''') + N' AS SCHEMA_NAME,' 
    + QUOTENAME(t.name, '''') + N' AS TABLE_NAME,' 
    + QUOTENAME(c.name,'''') + N' AS COLUMN_NAME,'
    + 'COUNT(*) AS TOTAL_ROW,'
    + 'COALESCE(SUM(CASE WHEN ' + QUOTENAME(c.name) + ' IS NULL THEN 1 ELSE 0 END),0) AS TOTAL_NULL '
    + 'FROM ' + QUOTENAME(s.name) + N'.' + QUOTENAME(t.name)
    , ' UNION ALL ') + N';'
    FROM sys.tables AS t
    JOIN sys.schemas AS s ON s.schema_id = t.schema_id
    JOIN sys.columns AS c ON c.object_id = t.object_id
    WHERE t.name IN (N'CTHD', N'HOADON', N'SANPHAM', N'KHACHHANG', N'NHANVIEN');
EXEC sp_executesql @SQL;
Dan Guzman
  • 43,250
  • 3
  • 46
  • 71
  • Thanks for your precious recommendations. – Dustin Nguyen Mar 21 '21 at 16:30
  • What does the amount of tables/columns have to do with anything, I would always go for the `union all`, or at the very least generate the dynamic SQL using `string_agg` rather than that horrible cursor – Charlieface Mar 21 '21 at 17:19
0

As Gordon said, use sp_execute properly to execute dynamics sql. And there's some others issue with this code, not related to the question.

  1. "the more important" when investigating is to use the print statement before the exec statement, to know what's being execute. Then, you'll realize where the errors is and why it won't work....

  2. the first execution of the loop is useless (or maybe this is the one that produce the error....). You initialize @Id with the value of 0 and then compare it with the value of the identity in the table @BANG, starting at 1. This result to @TABLE_NAME and @COLUMN_NAME set to NULL, thus, concatening string without using CONCAT will end up in a NULL value. Nothing is execute on the first loop.

However, using concat with null value will not result in a null value, but an incorrect value (query in your case). As an exemple, this code

SET @SQL = CONCAT('
INSERT INTO @RESULT
    VALUES (''', @TABLE_NAME, ''', ''', @COLUMN_NAME, ''', ''', @TOTAL_ROW, ', ', @TOTAL_NULL, ')';

will result in something like "INSERT INTO @RESULT VALUES (''CTHD'',''COL1'',,)" since both @TOTAL_ROW AND @TOTAL_NULL are null values. you need to use parametrized query using sp_executesql.

  1. There's no need to execute two count on the same table, one for total rows, the second for null values. select count(1) return the total number of rows, and select count(@column_name) return the number of non-null value. So, Count(1) - count(@column_name) will gives you the number of null value. Then, use something like this to insert into @result :

    INSERT INTO @RESULT (...) SELECT TABLE_NAME, COLUMN_NAME, COUNT(1), COUNT(1) - COUNT(@COLUMN_NAME) FROM ...

  2. when dealing with SQL Server object, you the quotename function. You'll never know when someone will put a space, quote, bracket or whatever in a schema/table/column name that might break you query.

  3. Do not use "WHILE @ID <= (SELECT COUNT(*) FROM @BANG)" to test if there are more rows to process. Use a "WHILE EXISTS (SELECT 1 FROM @BANG)"

MLeblanc
  • 1,816
  • 12
  • 21