Embedded SQL
Escrita de queries SQL em ADVPL/TLPP usando blocos BeginSQL/EndSQL com macros especiais para TOTVS Protheus
Embedded SQL in ADVPL/TLPP
Embedded SQL permite escrever queries SQL diretamente em codigo ADVPL/TLPP usando blocos BeginSQL ... EndSQL com expressoes macro especiais. Substitui a concatenacao de strings propensa a erros (cQuery += "SELECT...") por blocos SQL legiveis, type-safe e manteníveis.
Quando Usar
- Escrever qualquer query SQL em ADVPL/TLPP (preferir sobre concatenacao de strings)
- Necessidade de query com filtragem adequada de filial, tratamento de delecao e nomes de tabela
- Construir relatorios com SELECT, JOIN, GROUP BY complexos
- Qualquer situacao onde
TCQuerycom strings concatenadas e usado atualmente - Quando legibilidade e manutenibilidade do codigo SQL importa
BeginSQL vs TCQuery (Concatenacao de Strings)
| Aspecto | BeginSQL (Moderno) | TCQuery + Strings (Legado) |
|---|---|---|
| Legibilidade | SQL escrito naturalmente | SQL enterrado em concat de strings |
| Nomes de tabela | %table:SE2% automatico | RetSqlName("SE2") manual |
| Filtro de filial | %xfilial:SE2% automatico | xFilial("SE2") manual |
| Filtro de delecao | %notDel% automatico | D_E_L_E_T_ = ' ' manual |
| Binding de variaveis | %exp:cVar% | "'" + cVar + "'" (risco de SQL injection) |
| Tipos de colunas | column X as Date | TCSetField manual |
| Manutencao | Facil de ler e modificar | Dificil achar erros em strings |
| SQL Injection | Protegido via macros | Vulneravel se nao tiver cuidado |
Sempre preferir BeginSQL para codigo novo.
Sintaxe Principal
BeginSQL Alias cAlias
SELECT columns
FROM %table:ALIAS% ALIAS
WHERE ALIAS.%notDel%
AND ALIAS.FIELD_FILIAL = %xfilial:ALIAS%
AND ALIAS.FIELD = %exp:cVariable%
ORDER BY %Order:ALIAS%
EndSQLImportante: O parametro alias pode ser uma string literal ou GetNextAlias():
Local cAlias := GetNextAlias()
BeginSQL Alias cAlias
SELECT A1_COD, A1_LOJA, A1_NOME
FROM %table:SA1% SA1
WHERE SA1.%notDel%
AND SA1.A1_FILIAL = %xfilial:SA1%
EndSQL
DbSelectArea(cAlias)
While !Eof()
Conout(cAlias->A1_NOME)
DbSkip()
EndDo
DbCloseArea()Expressoes Macro Especiais
%table:TABLE%
Resolve o nome fisico da tabela com schema/owner adequado do banco de dados.
-- Input:
FROM %table:SA1% SA1
-- Expande para (exemplo):
FROM SA1010 SA1
-- (onde 010 = codigo da empresa, depende do ambiente)%xfilial:TABLE%
Retorna o valor da filial (branch) atual para a tabela.
-- Input:
AND SA1.A1_FILIAL = %xfilial:SA1%
-- Expande para:
AND SA1.A1_FILIAL = '01'
-- (ou '' se a tabela nao e filtrada por filial)%notDel%
Filtra registros logicamente deletados.
-- Input:
WHERE SA1.%notDel%
-- Expande para:
WHERE SA1.D_E_L_E_T_ <> '*'Sempre incluir. Protheus usa delecao logica, nao fisica.
%exp:EXPRESSION%
Faz binding de variaveis ADVPL, expressoes ou resultados de funcao no SQL.
Local cCodCli := "000001"
Local cLoja := "01"
Local dDataIni := CtoD("01/01/2026")
BeginSQL Alias cAlias
SELECT E2_PREFIXO, E2_NUM, E2_VALOR, E2_EMISSAO
FROM %table:SE2% SE2
WHERE SE2.%notDel%
AND SE2.E2_FILIAL = %xfilial:SE2%
AND SE2.E2_FORNECE = %exp:cCodCli%
AND SE2.E2_LOJA = %exp:cLoja%
AND SE2.E2_EMISSAO >= %exp:DtoS(dDataIni)%
EndSQLNota: %exp:% trata quoting automaticamente para campos character. Para datas, usar DtoS() para converter.
%Order:TABLE%
Retorna a ordenacao de chave primaria para a tabela.
-- Input:
ORDER BY %Order:SE2%
-- Expande para as colunas de chave primaria de SE2Declaracao de Tipo de Coluna
Declarar tipos de colunas do resultado para evitar chamadas manuais de TCSetField:
BeginSQL Alias cAlias
column E2_EMISSAO as Date
column E2_VENCTO as Date
column E2_VALOR as Numeric(16,2)
SELECT E2_PREFIXO, E2_NUM, E2_EMISSAO, E2_VENCTO, E2_VALOR
FROM %table:SE2% SE2
WHERE SE2.%notDel%
AND SE2.E2_FILIAL = %xfilial:SE2%
EndSQL
// Colunas sao automaticamente tipadas - sem necessidade de TCSetField
DbSelectArea(cAlias)
While !Eof()
Local dEmissao := (cAlias)->E2_EMISSAO // Ja tipo Date
Local nValor := (cAlias)->E2_VALOR // Ja Numeric
DbSkip()
EndDoSem declaracao column, todos os campos retornam como Character e precisam de conversao manual.
Padroes de JOIN
INNER JOIN
BeginSQL Alias cAlias
SELECT SE2.E2_PREFIXO, SE2.E2_NUM, SE2.E2_VALOR,
SA2.A2_NOME, SA2.A2_CGC
FROM %table:SE2% SE2
INNER JOIN %table:SA2% SA2
ON SE2.E2_FORNECE = SA2.A2_COD
AND SE2.E2_LOJA = SA2.A2_LOJA
AND SA2.A2_FILIAL = %xfilial:SA2%
AND SA2.%notDel%
WHERE SE2.%notDel%
AND SE2.E2_FILIAL = %xfilial:SE2%
AND SE2.E2_EMISSAO >= %exp:DtoS(dDataIni)%
ORDER BY SE2.E2_EMISSAO
EndSQLLEFT JOIN
BeginSQL Alias cAlias
SELECT SC5.C5_NUM, SC5.C5_CLIENTE, SC5.C5_LOJACLI,
SA1.A1_NOME
FROM %table:SC5% SC5
LEFT JOIN %table:SA1% SA1
ON SC5.C5_CLIENTE = SA1.A1_COD
AND SC5.C5_LOJACLI = SA1.A1_LOJA
AND SA1.A1_FILIAL = %xfilial:SA1%
AND SA1.%notDel%
WHERE SC5.%notDel%
AND SC5.C5_FILIAL = %xfilial:SC5%
EndSQLBoa pratica: Sempre usar sintaxe de JOIN explicito, nao FROM separado por virgula.
Padroes de Agregacao
SUM / COUNT / AVG
BeginSQL Alias cAlias
column TOTAL as Numeric(16,2)
column QTD as Numeric(10,0)
SELECT SE2.E2_FORNECE, SE2.E2_LOJA,
SUM(SE2.E2_VALOR) AS TOTAL,
COUNT(*) AS QTD
FROM %table:SE2% SE2
WHERE SE2.%notDel%
AND SE2.E2_FILIAL = %xfilial:SE2%
AND SE2.E2_EMISSAO BETWEEN %exp:DtoS(dDataIni)% AND %exp:DtoS(dDataFim)%
GROUP BY SE2.E2_FORNECE, SE2.E2_LOJA
HAVING SUM(SE2.E2_VALOR) > 0
ORDER BY TOTAL DESC
EndSQLSubquery
BeginSQL Alias cAlias
SELECT SA1.A1_COD, SA1.A1_NOME
FROM %table:SA1% SA1
WHERE SA1.%notDel%
AND SA1.A1_FILIAL = %xfilial:SA1%
AND SA1.A1_COD IN (
SELECT SC5.C5_CLIENTE
FROM %table:SC5% SC5
WHERE SC5.%notDel%
AND SC5.C5_FILIAL = %xfilial:SC5%
AND SC5.C5_EMISSAO >= %exp:DtoS(dDataIni)%
)
EndSQLPadrao Completo: Query com Iteracao de Cursor
#Include "TOTVS.CH"
#Include "TopConn.ch"
User Function QueryEx()
Local cAlias := GetNextAlias()
Local aArea := GetArea()
Local aResult := {}
Local dDataIni := Date() - 30
BeginSQL Alias cAlias
column E2_EMISSAO as Date
column E2_VENCTO as Date
column E2_VALOR as Numeric(16,2)
column SALDO as Numeric(16,2)
SELECT SE2.E2_PREFIXO, SE2.E2_NUM, SE2.E2_PARCELA,
SE2.E2_FORNECE, SE2.E2_LOJA,
SE2.E2_EMISSAO, SE2.E2_VENCTO,
SE2.E2_VALOR,
(SE2.E2_VALOR - SE2.E2_PAGO) AS SALDO,
SA2.A2_NOME
FROM %table:SE2% SE2
INNER JOIN %table:SA2% SA2
ON SE2.E2_FORNECE = SA2.A2_COD
AND SE2.E2_LOJA = SA2.A2_LOJA
AND SA2.A2_FILIAL = %xfilial:SA2%
AND SA2.%notDel%
WHERE SE2.%notDel%
AND SE2.E2_FILIAL = %xfilial:SE2%
AND SE2.E2_EMISSAO >= %exp:DtoS(dDataIni)%
AND (SE2.E2_VALOR - SE2.E2_PAGO) > 0
ORDER BY SE2.E2_VENCTO
EndSQL
DbSelectArea(cAlias)
While !(cAlias)->(Eof())
aAdd(aResult, {;
(cAlias)->E2_PREFIXO,;
(cAlias)->E2_NUM,;
(cAlias)->E2_VALOR,;
(cAlias)->SALDO,;
(cAlias)->A2_NOME;
})
(cAlias)->(DbSkip())
EndDo
(cAlias)->(DbCloseArea())
RestArea(aArea)
Return aResultRestricoes e Armadilhas
| Regra | Por que |
|---|---|
NAO iniciar uma linha com * dentro de BeginSQL | Pre-compilador ADVPL trata * no inicio da linha como comentario |
Sempre incluir %notDel% | Protheus usa delecao logica, nunca confiar em fisica |
Sempre incluir %xfilial% ou filtro de filial | Ambientes multi-filial retornarao dados errados |
Usar GetNextAlias() para o alias | Evita conflitos de nome de alias com work areas abertas |
Sempre DbCloseArea() ao terminar | Previne vazamento de work areas (numero limitado de aliases) |
NAO usar SELECT * | Especificar colunas explicitamente para performance e clareza |
Valores de data devem usar DtoS() | Banco armazena datas como strings YYYYMMDD |
Declaracoes column vao ANTES do SELECT | Definem tipos do resultado, nao colunas da query |
Operacoes DML
Para INSERT, UPDATE, DELETE usar TCSqlExec em vez de BeginSQL:
// INSERT
Local cSql := "INSERT INTO " + RetSqlName("ZZ1") + " "
cSql += "(ZZ1_FILIAL, ZZ1_CODIGO, ZZ1_DESCRI, D_E_L_E_T_, R_E_C_N_O_) "
cSql += "VALUES ('" + xFilial("ZZ1") + "', '001', 'Teste', ' ', " + cValToChar(GetSxeNum("ZZ1","ZZ1_CODIGO")) + ")"
nRet := TCSqlExec(cSql)
// UPDATE
cSql := "UPDATE " + RetSqlName("ZZ1") + " SET "
cSql += "ZZ1_DESCRI = 'Novo Valor' "
cSql += "WHERE ZZ1_FILIAL = '" + xFilial("ZZ1") + "' "
cSql += "AND ZZ1_CODIGO = '001' "
cSql += "AND D_E_L_E_T_ = ' '"
nRet := TCSqlExec(cSql)
If nRet < 0
Conout("SQL Error: " + TCSqlError())
EndIfBeginSQL e somente para SELECT. Usar TCSqlExec para DML (INSERT/UPDATE/DELETE).
Dicas de Performance
- Usar declaracoes
columnpara evitar conversoes de tipo pos-query - Usar JOINs em vez de subqueries quando possivel
- Filtrar cedo com WHERE, nao apos ler todas as linhas em ADVPL
- Usar hint NOLOCK para queries somente leitura (SQL Server):
FROM %table:SA1% SA1 WITH(NOLOCK) - Usar TOP N quando precisar apenas de resultados limitados
- Fechar aliases imediatamente apos uso para liberar work areas
- Evitar loops com BeginSQL dentro - construir uma query que retorna todos os dados necessarios
Migracao de TCQuery para BeginSQL
Antes (concatenacao de strings):
Local cQuery := ""
cQuery += "SELECT A1_COD, A1_NOME "
cQuery += "FROM " + RetSqlName("SA1") + " SA1 "
cQuery += "WHERE SA1.D_E_L_E_T_ = ' ' "
cQuery += "AND A1_FILIAL = '" + xFilial("SA1") + "' "
cQuery += "AND A1_TIPO = '" + cTipo + "'"
TCQuery cQuery New Alias "QRY_CLI"Depois (Embedded SQL):
Local cAlias := GetNextAlias()
BeginSQL Alias cAlias
SELECT A1_COD, A1_NOME
FROM %table:SA1% SA1
WHERE SA1.%notDel%
AND SA1.A1_FILIAL = %xfilial:SA1%
AND SA1.A1_TIPO = %exp:cTipo%
EndSQLBeneficios: Sem RetSqlName manual, sem D_E_L_E_T_, sem xFilial(), sem quoting de strings, sem risco de SQL injection.