¡Hola, chicos!
En este artículo quiero traerles una solución que creé ayer para incluirla en una lista de verificación que estoy desarrollando, que me permite identificar la última versión del Service Pack (SP) y la Actualización acumulativa (CU) para la versión de SQL Server en la que estoy ejecutando este código.

Para identificar las últimas actualizaciones de cada versión, utilizaré el sitio web https://sqlserverbuilds.blogspot.com/, que siempre está actualizado con las últimas actualizaciones de SQL Server (también podría ser el https://buildnumbers.wordpress.com/sqlserver/).

Para extraer datos de la página a SQL Server, usaré el recurso Automatización OLE, en el que ya he publicado varios artículos explicando cómo realizar WebScraping de páginas WEB y API utilizando este recurso.

Después de recuperar el código fuente HTML de la página, necesito identificar la tabla que contiene los datos según la versión que estoy buscando y también necesito realizar algunos tratamientos en el código devuelto para corregir algunas inconsistencias:

  • Conversión de entidades HTML (ej:  )
  • Atributos de etiqueta sin comillas (por ejemplo, class=sp en lugar de class=”sp” = HTML no válido)
  • Etiquetas abiertas y no cerradas (por ejemplo, Columna1Columna2 en lugar de Columna1Columna2)

Esta termina siendo la parte más laboriosa de este script, porque si no se resuelven estas inconsistencias en el código, SQL Server devuelve un error al intentar convertir XHTML a XML.

Luego de realizar todos estos tratamientos y convertir el código XHTML a XML, aplico comandos XQuery para identificar la primera fila de la tabla de la respectiva versión de SQL Server y la devuelvo en SP.

código fuente

Después de un breve resumen de lo que necesitaba hacer para realizar esta tarea, pondré a disposición el código fuente de la solución creada a continuación, que se puede utilizar con un simple "F5".

IF (OBJECT_ID('dbo.stpRetorna_Ultima_Versao_SQL') IS NULL) EXEC('CREATE PROCEDURE dbo.stpRetorna_Ultima_Versao_SQL AS SELECT 1')
GO

ALTER PROCEDURE dbo.stpRetorna_Ultima_Versao_SQL (
    @Versao VARCHAR(20) = NULL
)
AS
BEGIN


    --------------------------------------------------------------------------------
    -- Habilitando o OLE Automation (Se não estiver ativado)
    --------------------------------------------------------------------------------

    DECLARE @Fl_Ole_Automation_Ativado BIT = (SELECT (CASE WHEN CAST([value] AS VARCHAR(MAX)) = '1' THEN 1 ELSE 0 END) FROM sys.configurations WHERE [name] = 'Ole Automation Procedures')

    IF (@Fl_Ole_Automation_Ativado = 0)
    BEGIN
 
        EXECUTE sp_configure 'show advanced options', 1
        RECONFIGURE WITH OVERRIDE
    
        EXEC sp_configure 'Ole Automation Procedures', 1
        RECONFIGURE WITH OVERRIDE
    
    END


    
    DECLARE 
        @obj INT,
        @Url VARCHAR(8000),
        @xml VARCHAR(MAX),
        @resposta VARCHAR(MAX)
        
    SET @Url = 'http://sqlserverbuilds.blogspot.com/'
 
    EXEC sys.sp_OACreate 'MSXML2.ServerXMLHTTP', @obj OUT
    EXEC sys.sp_OAMethod @obj, 'open', NULL, 'GET', @Url, false
    EXEC sys.sp_OAMethod @obj, 'send'
 
 
    DECLARE @xml_versao_sql TABLE (
        Ds_Dados VARCHAR(MAX)
    )
 
    INSERT INTO @xml_versao_sql(Ds_Dados)
    EXEC sys.sp_OAGetProperty @obj, 'responseText' --, @resposta OUT
    
    
    EXEC sys.sp_OADestroy @obj



    --------------------------------------------------------------------------------
    -- Desativando o OLE Automation (Se não estava habilitado antes)
    --------------------------------------------------------------------------------

    IF (@Fl_Ole_Automation_Ativado = 0)
    BEGIN
 
        EXEC sp_configure 'Ole Automation Procedures', 0
        RECONFIGURE WITH OVERRIDE
 
        EXECUTE sp_configure 'show advanced options', 0
        RECONFIGURE WITH OVERRIDE
 
    END
 
    

    DECLARE
        @Versao_SQL_Build VARCHAR(20)

    IF (@Versao IS NOT NULL)
        SET @Versao_SQL_Build = @Versao

    ELSE BEGIN

        SET @Versao_SQL_Build = (CASE LEFT(CONVERT(VARCHAR, SERVERPROPERTY('ProductVersion')), 2)
            WHEN '8.' THEN '2000'
            WHEN '9.' THEN '2005'
            WHEN '10' THEN (
                CASE
                    WHEN LEFT(CONVERT(VARCHAR, SERVERPROPERTY('ProductVersion')), 4) = '10.5' THEN '2008 R2' 
                    WHEN LEFT(CONVERT(VARCHAR, SERVERPROPERTY('ProductVersion')), 4) = '10.0' THEN '2008' 
                END)
            WHEN '11' THEN '2012'
            WHEN '12' THEN '2014'
            WHEN '13' THEN '2016'
            WHEN '14' THEN '2017'
            WHEN '15' THEN '2019'
            ELSE '2019'
        END)

    END


    SELECT TOP 1 @resposta = Ds_Dados FROM @xml_versao_sql
 
    
    SET @xml = @resposta COLLATE SQL_Latin1_General_CP1251_CS_AS

    DECLARE
        @PosicaoInicialVersao INT,
        @PosicaoFinalVersao INT,
        @ExpressaoBuscar VARCHAR(100) = 'Microsoft SQL Server ' + @Versao_SQL_Build + ' Builds',
        @RetornoTabela VARCHAR(MAX),
        @dadosXML XML

    SET @PosicaoInicialVersao = CHARINDEX(@ExpressaoBuscar, @xml) + LEN(@ExpressaoBuscar) + 6
    SET @PosicaoFinalVersao = CHARINDEX('</table>', @xml, @PosicaoInicialVersao)
    SET @RetornoTabela = SUBSTRING(@xml, @PosicaoInicialVersao, @PosicaoFinalVersao - @PosicaoInicialVersao + 8)


    -- Corrigindo classes sem aspas duplas ("")
    SET @RetornoTabela = REPLACE(@RetornoTabela, ' border=1 cellpadding=4 cellspacing=0 bordercolor="#CCCCCC" style="border-collapse:collapse"', '')
    SET @RetornoTabela = REPLACE(@RetornoTabela, ' target=_blank rel=nofollow', ' target="_blank" rel="nofollow"')
    SET @RetornoTabela = REPLACE(@RetornoTabela, ' class=h', '')
    SET @RetornoTabela = REPLACE(@RetornoTabela, ' class=lsp', '')
    SET @RetornoTabela = REPLACE(@RetornoTabela, ' class=cu', '')
    SET @RetornoTabela = REPLACE(@RetornoTabela, ' class=sp', '')
    SET @RetornoTabela = REPLACE(@RetornoTabela, ' class=rtm', '')
    SET @RetornoTabela = REPLACE(@RetornoTabela, ' width=580', '')
    SET @RetornoTabela = REPLACE(@RetornoTabela, ' width=125', '')
    SET @RetornoTabela = REPLACE(@RetornoTabela, ' class=lcu', '')
    SET @RetornoTabela = REPLACE(@RetornoTabela, ' class=cve', '')
    SET @RetornoTabela = REPLACE(@RetornoTabela, ' class=lrtm', '')
    SET @RetornoTabela = REPLACE(@RetornoTabela, ' class=beta', '')

    -- Corrigindo elementos não fechados corretamente
    SET @RetornoTabela = REPLACE(@RetornoTabela, '<th>', '</th><th>')
    SET @RetornoTabela = REPLACE(@RetornoTabela, '<tr></th>', '<tr>')
    SET @RetornoTabela = REPLACE(@RetornoTabela, '<th>Build<th ', '<th>Build</th><th ')
    SET @RetornoTabela = REPLACE(@RetornoTabela, '<th>Release Date</tr>', '<th>Release Date</th></tr>')

    SET @RetornoTabela = REPLACE(@RetornoTabela, '<td>', '</td><td>')
    SET @RetornoTabela = REPLACE(@RetornoTabela, '<tr></td>', '<tr>')

    SET @RetornoTabela = REPLACE(@RetornoTabela, '</tr>', '</td></tr>')
    SET @RetornoTabela = REPLACE(@RetornoTabela, '</th></td>', '</th>')
    SET @RetornoTabela = REPLACE(@RetornoTabela, '</td></td>', '</td>')

    -- Removendo elementos de entidades HTML
    SET @RetornoTabela = REPLACE(@RetornoTabela, ' ', ' ')
    SET @RetornoTabela = REPLACE(@RetornoTabela, '&kbln', '&kbln')
    SET @RetornoTabela = REPLACE(@RetornoTabela, '<br>', '<br/>')

    SET @dadosXML = CONVERT(XML, @RetornoTabela)


    DECLARE @Atualizacoes_SQL_Server TABLE
    (
        [Ultimo_Build] VARCHAR(100),
        [Ultimo_Build_SQLSERVR.EXE] VARCHAR(100),
        [Versao_Arquivo] VARCHAR(100),
        [Q] VARCHAR(100),
        [KB] VARCHAR(100),
        [Descricao_KB] VARCHAR(500),
        [Lancamento_KB] VARCHAR(100),
        [Download_Ultimo_Build] VARCHAR(100)
    )


    INSERT INTO @Atualizacoes_SQL_Server
    SELECT
        @dadosXML.value('(//table/tr/td[1])[1]','varchar(100)') AS Ultimo_Build,
        @dadosXML.value('(//table/tr/td[2])[1]','varchar(100)') AS [Ultimo_Build_SQLSERVR.EXE],
        @dadosXML.value('(//table/tr/td[3])[1]','varchar(100)') AS Versao_Arquivo,
        @dadosXML.value('(//table/tr/td[4])[1]','varchar(100)') AS [Q],
        @dadosXML.value('(//table/tr/td[5])[1]','varchar(100)') AS KB,
        @dadosXML.value('(//table/tr/td[6]/a)[1]','varchar(500)') AS Descricao_KB,
        @dadosXML.value('(//table/tr/td[7])[1]','varchar(100)') AS Lancamento_KB,
        @dadosXML.value('(//table/tr/td[6]/a/@href)[1]','varchar(100)') AS Download_Ultimo_Build
    

    DECLARE 
        @Url_Ultima_Versao_SQL VARCHAR(500) = (SELECT TOP(1) Download_Ultimo_Build FROM @Atualizacoes_SQL_Server),
        @Ultimo_Build VARCHAR(100) = (SELECT TOP(1) Ultimo_Build FROM @Atualizacoes_SQL_Server),
        @Descricao_KB VARCHAR(500) = (SELECT TOP(1) Descricao_KB FROM @Atualizacoes_SQL_Server)


    IF (@Versao IS NOT NULL)
    BEGIN

        SELECT * 
        FROM @Atualizacoes_SQL_Server

    END
    ELSE BEGIN

        IF (CONVERT(VARCHAR(100), SERVERPROPERTY('ProductVersion')) >= @Ultimo_Build)
            SELECT 'SQL Server ATUALIZADO!' AS SQL_Server_Atualizado
        ELSE BEGIN
    
            SELECT 'SQL Server DESATUALIZADO!!' AS SQL_Server_Desatualizado
            UNION ALL
            SELECT '----------------------------------------'
            UNION ALL
            SELECT 'Versão mais atual: ' + @Ultimo_Build + ' (' + @Descricao_KB + ')'
            UNION ALL
            SELECT 'Download da versão mais atual: '
            UNION ALL
            SELECT @Url_Ultima_Versao_SQL
            UNION ALL
            SELECT '----------------------------------------'
            UNION ALL 
            SELECT 'Sua versão: ' + CONVERT(VARCHAR(100), SERVERPROPERTY('ProductVersion')) + ' (' + CONVERT(VARCHAR(100), SERVERPROPERTY('ProductLevel')) + ')'

            SELECT * 
            FROM @Atualizacoes_SQL_Server

        END

    END


END

Ejemplos de uso

Ahora demostraré algunos ejemplos de cómo funciona este procedimiento almacenado en la práctica.

Recuperar la última actualización de un SQL Server 2017 actualizado

SELECT @@VERSION

-- Retorna a última versão do SQL Server instalado na instância atual
EXEC dbo.stpRetorna_Ultima_Versao_SQL 

Resultado:

Recuperando la última actualización de SQL Server 2017

SELECT @@VERSION

-- Retorna a última versão do SQL Server instalado na instância atual
EXEC dbo.stpRetorna_Ultima_Versao_SQL 

Resultado:

Recuperar la última actualización de SQL Server 2008 R2

SELECT @@VERSION

-- Retorna a última versão do SQL Server instalado na instância atual
EXEC dbo.stpRetorna_Ultima_Versao_SQL 

Resultado:

Recuperar la última actualización de SQL Server 2019

SELECT @@VERSION

-- Retorna a última versão do SQL Server instalado na instância atual
EXEC dbo.stpRetorna_Ultima_Versao_SQL 

Resultado:

Necesito mostrar TODAS las actualizaciones para una versión de SQL Server

Si desea mostrar todas las actualizaciones para la versión deseada de SQL Server y no solo la última, puede usar el siguiente código (después de la línea 150):

SELECT 
    Versoes.linha.value('(td[1])[1]','varchar(100)') AS Ultimo_Build,
    Versoes.linha.value('(td[2])[1]','varchar(100)') AS [Ultimo_Build_SQLSERVR.EXE],
    Versoes.linha.value('(td[3])[1]','varchar(100)') AS Versao_Arquivo,
    Versoes.linha.value('(td[4])[1]','varchar(100)') AS [Q],
    Versoes.linha.value('(td[5])[1]','varchar(100)') AS KB,
    (CASE 
        WHEN Versoes.linha.value('(td[6]/a)[1]','varchar(500)') IS NOT NULL THEN Versoes.linha.value('(td[6]/a)[1]','varchar(500)')
        ELSE Versoes.linha.value('(td[6])[1]','varchar(500)')
    END) AS Descricao_KB,
    Versoes.linha.value('(td[7])[1]','varchar(100)') AS Lancamento_KB,
    Versoes.linha.value('(td[6]/a/@href)[1]','varchar(100)') AS Download_Ultimo_Build
FROM
    @dadosXML.nodes('//table/tr') Versoes(linha)
WHERE
    Versoes.linha.value('(td[1])[1]','varchar(100)') IS NOT NULL

Resultado:

Conclusión

Bueno chicos, espero que les haya gustado este artículo. Lo estoy empezando a utilizar en ambientes para hacer un chequeo rápido y me terminó siendo muy práctico. Si desea crear un monitoreo para esto en su entorno, analizando todas las instancias, puede crear este procedimiento almacenado en todas las instancias y usar los datos devueltos para enviar correos electrónicos automáticos según sea necesario.

En este escenario de monitorización, otra solución muy interesante es la Reginaldo Silva, quien hizo la publicación Get-SQLBuildVersion una forma diferente de mantenerse actualizado, donde compartió una solución escrita en PowerShell, que desarrolló, donde pasa el nombre de la instancia como parámetro y el script ya realiza esta validación y parámetros para enviar el resultado por correo electrónico. Muy práctico para utilizar en múltiples instancias.

Al igual que Reginaldo, destacaré el artículo. Actualización acumulativa, aplicar o no, ¡esa es la cuestión! de Luciano Moreira (Luti), donde explica sus opiniones sobre la aplicación constante de actualizaciones de SQL Server (estoy de acuerdo con él), ya que muchos DBA's no tienen por costumbre mantener actualizado SQL Server por miedo a introducir un nuevo error tras aplicar una actualización.

En mi opinión, esto no tiene mucho sentido hoy en día, ya que estos eventos son muy raros y, al aplicar siempre actualizaciones, su entorno siempre está actualizado con nuevas funciones, correcciones de errores y mejoras de seguridad, además de tener un proceso de solicitud de actualizaciones más maduro y común en el día a día de la empresa.

Oh, vale la pena recordar que a partir de SQL Server 2017, ya no tendremos Service Packs, solo Actualizaciones acumulativas, como está documentado. en este artículo aquí.

Eso es todo, amigos.
Espero que hayas disfrutado de este artículo y ¡hasta luego!