El autor de este artículo compartió las ideas para solucionar el problema de que el resultado de la consulta no cumple con las expectativas o es "incorrecto" debido a la conversión implícita de los tipos de tiempo y valor de Oceanbase.
Autor: Ren Zhongyu
Un miembro del equipo de Acson DBA, bueno en el análisis de fallas y la optimización del rendimiento, bienvenido a discutir problemas técnicos relacionados con el artículo.
Fuente de este artículo: contribución original
- Producido por la comunidad de código abierto de Aikesheng, no se permite usar el contenido original sin autorización, comuníquese con el editor e indique la fuente para la reimpresión.
Encontré un problema de conversión implícita de tipo de datos "confuso" en el uso de OceanBase anteriormente. La conclusión es relativamente simple, por lo que me gustaría compartir con ustedes las ideas de investigación.
Descripción del problema
Cuando un equipo de proyecto del cliente ejecuta la instrucción SQL de actualización, ocasionalmente fallará y el error es el siguiente:
Después de la desensibilización
ERROR mala gramática de SQL [actualizar renzy establecido en = marca de tiempo actual, expirar_ en = (cast (unix_marca de tiempo (marca de tiempo actual (3) como sin firmar) +?)), order_id =? donde id = ? y (expire_at < current_timestamp or order_id = ?)] java.sql.SQLSyntaxErrorException: (conn=1277168) Valor incorrecto.
Consulta la versión de OceanBase.
./observer -V
observer (OceanBase 3.2.3.2)
REVISION: 105000092022092216-445151f0edb502e00ae5839dfd92627816b2b822
Ver la estructura de la tabla y los datos.
MySQL [test]> show create table renzy\G
*************************** 1. row ***************************
Table: renzy
Create Table: CREATE TABLE `renzy` (
`id` varchar(64) NOT NULL,
`at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`order_id` varchar(64) NOT NULL,
`expire_at` bigint(20) NOT NULL,
`vt` timestamp NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARSET = utf8mb4 ROW_FORMAT = DYNAMIC COMPRESSION = 'zstd_1.3.8' REPLICA_NUM = 3 BLOCK_SIZE = 16384 USE_BLOOM_FILTER = FALSE TABLET_SIZE = 134217728 PCTFREE = 0
MySQL [test]> select * from renzy;
+----+---------------------------+---------------+------------+---------------------------------+
| id | at | order_id | expire_at | vt |
+----+---------------------------+---------------+------------+---------------------------------+
| 1 | 2023-07-07 14:57:13 | 0:[email protected] | 1716040750 | 2023-07-07 14:57:13 |
+----+---------------------------+---------------+------------+---------------------------------+
1 row in set (0.02 sec)
Solución de problemas
Pregunta 1: declaración de error
Ejecute la instrucción SQL que informó el error directamente.
update renzy set at=CURRENT_TIMESTAMP, expire_at=(cast(unix_timestamp(current_timestamp(3)) as unsigned) + 30000000), order_id= '0:[email protected]' where id = '1' and (expire_at < CURRENT_TIMESTAMP or order_id = '0:[email protected]')
ERROR 1292 (22007): Incorrect values.
Pregunta 2
Sin embargo, cuando la identificación de la clave principal no coincide con ningún dato, UPDATE
la declaración no informará un error.
¿por qué? Primero registre el problema y luego mire el registro.
# 表不存在 id=2 的数据
update renzy set acquired_at=CURRENT_TIMESTAMP, expire_at=(cast(unix_timestamp(current_timestamp(3)) as unsigned) + 30000000), order_id= '0:[email protected]' where id = '2' and (expire_at < CURRENT_TIMESTAMP or order_id = '0:[email protected]')
Query OK.
Salida de sql_auit y registro de error SQL
Analice la salida con el problema sql$gv_audit
y obtenga la siguiente información:
- El plan de declaración es un plan local, distribuido al nodo 0.71.
- El código de error es 4219 (
datetime
valor no válido).
MySQL [oceanbase]> select trace_id,svr_ip,ret_code,retry_cnt,usec_to_time(request_time),elapsed_time,execute_time,plan_type,query_sql from gv$sql_audit where query_sql like 'update id%' and ret_code != 0 order by request_time desc limit 5;
+-----------------------------------+-------------+----------+-----------+----------------------------+--------------+--------------+-----------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| trace_id | svr_ip | ret_code | retry_cnt | usec_to_time(request_time) | elapsed_time | execute_time | plan_type | query_sql |
+-----------------------------------+-------------+----------+-----------+----------------------------+--------------+--------------+-----------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| YB420CF10047-0005FBCCEF6E3635-0-0 | 12.241.0.71 | -4219 | 0 | 2023-06-06 15:32:08.375051 | 689 | 611 | 1 | update id set acquired_at=CURRENT_TIMESTAMP, expire_at=(cast(unix_timestamp(current_timestamp(3)) as unsigned) + 30000000), order_id= '0:[email protected]' where id = '2' and (expire_at < CURRENT_TIMESTAMP or order_id = '0:[email protected]') |
·····
5 rows in set (5.21 sec)
Registro de errores clave
0,72 obs.log
.
#grep YB420CF10047-0005FBCCEF6E3635-0-0 observer.log.20230606153309
[2023-06-06 15:32:08.375202] WARN [LIB.TIME] int_to_ob_time_with_date (ob_time_convert.cpp:1618) [38763][0][YB420CF10047-0005FBCCEF6E3635-0-0] [lt=5] [dc=0] datetime is invalid or out of range(ret=-4219, int64=0)
[2023-06-06 15:32:08.375211] WARN [LIB.TIME] int_to_datetime (ob_time_convert.cpp:329) [38763][0][YB420CF10047-0005FBCCEF6E3635-0-0] [lt=8] [dc=0] failed to convert integer to datetime(ret=-4219)
[2023-06-06 15:32:08.375214] WARN [SQL] common_int_datetime (ob_datum_cast.cpp:709) [38763][0][YB420CF10047-0005FBCCEF6E3635-0-0] [lt=3] [dc=0] int_datetime failed(ret=-4219)
[2023-06-06 15:32:08.375219] WARN [SQL] int_datetime (ob_datum_cast.cpp:2076) [38763][0][YB420CF10047-0005FBCCEF6E3635-0-0] [lt=4] [dc=0] fail to exec common_int_datetime(expr, in_val, ctx, res_datum)(ret=-4219, in_val=1716036728)
[2023-06-06 15:32:08.375223] WARN [SQL] get_comparator_operands (ob_expr_operator.h:1131) [38763][0][YB420CF10047-0005FBCCEF6E3635-0-0] [lt=4] [dc=0] left eval failed(ret=-4219)
[2023-06-06 15:32:08.375227] WARN [SQL.ENG] def_relational_eval_func (ob_expr_cmp_func.cpp:54) [38763][0][YB420CF10047-0005FBCCEF6E3635-0-0] [lt=4] [dc=0] failed to eval args(ret=-4219)
[2023-06-06 15:32:08.375232] WARN [SQL.ENG] calc_or_exprN (ob_expr_or.cpp:221) [38763][0][YB420CF10047-0005FBCCEF6E3635-0-0] [lt=5] [dc=0] eval arg 0 failed(ret=-4219)
[2023-06-06 15:32:08.375240] WARN [SQL.ENG] filter_row (ob_operator.cpp:915) [38763][0][YB420CF10047-0005FBCCEF6E3635-0-0] [lt=7] [dc=0] expr evaluate failed(ret=-4219, expr=0x7f37808ec058)
[2023-06-06 15:32:08.375247] WARN [STORAGE] check_filtered (ob_multiple_merge.cpp:1249) [38763][0][YB420CF10047-0005FBCCEF6E3635-0-0] [lt=7] [dc=0] filter row failed(ret=-4219)
[2023-06-06 15:32:08.375251] WARN [STORAGE] process_fuse_row (ob_multiple_merge.cpp:787) [38763][0][YB420CF10047-0005FBCCEF6E3635-0-0] [lt=3] [dc=0] fail to check row filtered(ret=-4219)
Se observa que el método que arroja el error al principio int_to_ob_time_with_date
es "datetime is invalid or out of range(ret=-4219, int64=0)" , es decir el valor no es válido o excede el rango.
El enlace de llamada aquí UPDATE
es:
common_int_datetime -> int_to_datetime -> int_to_ob_time_with_date
Pregunta 3: Tratando de usar los resultados de la consulta SELECT
La hora futura (1716040750) se almacena en la tabla EXPIRE_AT
Comparada con la hora actual (1686042749), el resultado de la consulta no debería mostrar el resultado, ¿verdad? (Registrado por primera vez como Pregunta 3: El resultado de la consulta no cumple con las expectativas)
# 表中只有 1 行记录,且 EXPIRE_AT 的值为 1716040750
MySQL [mock_db]> select * from renzy where EXPIRE_AT < CURRENT_TIMESTAMP;
+-------+---------------------+---------------------+------------+------------------------------------+
| id | ACQUIRED_AT | order_id | EXPIRE_AT | LST_UP_TM |
+-------+---------------------+---------------------+------------+------------------------------------+
| 1 | 2023-06-06 16:39:10 | 0:[email protected] | 1716040750 | 2023-06-06 16:39:10 |
+-------+---------------------+---------------------+------------+------------------------------------+
1 row in set (0.05 sec)
# 当前时间戳
MySQL [mock_db]> select unix_timestamp(CURRENT_TIMESTAMP);
+--------------------------------------------+
| unix_timestamp(CURRENT_TIMESTAMP) |
+--------------------------------------------+
| 1686042749 |
+--------------------------------------------+
1 row in set (0.03 sec)
SELECT
Registro de errores de consulta.
[2023-06-06 17:08:54.307371] WARN [LIB.TIME] int_to_ob_time_with_date (ob_time_convert.cpp:1618) [38763][0][YB420CF10047-0005FBCCEF6F9E6D-0-0] [lt=10] [dc=0] datetime is invalid or out of range(ret=-4219, int64=0)
[2023-06-06 17:08:54.307382] WARN [LIB.TIME] int_to_datetime (ob_time_convert.cpp:329) [38763][0][YB420CF10047-0005FBCCEF6F9E6D-0-0] [lt=10] [dc=0] failed to convert integer to datetime(ret=-4219)
El enlace de llamada aquí SELECT
: int_to_datetime -> int_to_ob_time_with_date
Las anteriores son algunas preguntas que están en duda.Antes del análisis específico, entendamos los puntos de conocimiento previo: la conversión implícita de OceanBase.
Conversión implícita de OceanBase
Los valores de los tipos de datos bigint
y datetime
no se pueden comparar directamente. Es necesario convertir int
primero al tipo de tiempo. Esta es la llamada conversión implícita, por lo que la forma en que OceanBase se convierte aquí es muy importante.
-
int
La conversión de tipo al tipo de hora reconocido por OceanBase (es decir, OBTime) no utilizafrom_unixtime
esta función, sino que utiliza la propia lógica interna de OceanBase. -
Involucrado en el código fuente
int
, la lógica de conversión implícitadouble
destring
tipo es la siguiente:
int_to_datetime
////////////////////////////////
// int / double / string -> datetime / date / time / year.
int ObTimeConverter::int_to_datetime(int64_t int_part, int64_t dec_part,
const ObTimeConvertCtx &cvrt_ctx, int64_t &value,
const ObDateSqlMode date_sql_mode)
{
int ret = OB_SUCCESS;
dec_part = (dec_part + 500) / 1000;
if (0 == int_part) {
value = ZERO_DATETIME;
} else {
ObTime ob_time(DT_TYPE_DATETIME);
ObDateSqlMode local_date_sql_mode = date_sql_mode;
if (cvrt_ctx.is_timestamp_) {
local_date_sql_mode.allow_invalid_dates_ = false;
}
if (OB_FAIL(int_to_ob_time_with_date(int_part, ob_time, false, local_date_sql_mode))) {
LOG_WARN("failed to convert integer to datetime", K(ret));
} else if (OB_FAIL(ob_time_to_datetime(ob_time, cvrt_ctx, value))) {
LOG_WARN("failed to convert datetime to seconds", K(ret));
}
}
value += dec_part;
if (OB_SUCC(ret) && !is_valid_datetime(value)) {
ret = OB_DATETIME_FUNCTION_OVERFLOW;
LOG_WARN("datetime filed overflow", K(ret), K(value));
}
return ret;
}
Finalmente llamó int_to_ob_time_with_date
:
////////////////////////////////
// int / uint / string -> ObTime / ObInterval <- datetime / date / time.
int ObTimeConverter::int_to_ob_time_with_date(int64_t int64, ObTime &ob_time, bool is_dayofmonth,
const ObDateSqlMode date_sql_mode)
{
int ret = OB_SUCCESS;
int32_t *parts = ob_time.parts_;
if (is_dayofmonth && 0 == int64) {
parts[DT_SEC] = 0;
parts[DT_MIN] = 0;
parts[DT_HOUR] = 0;
parts[DT_MDAY] = 0;
parts[DT_MON] = 0;
parts[DT_YEAR] = 0;
} else if (int64 < power_of_10[2]) {
ret = OB_INVALID_DATE_VALUE;
LOG_WARN("datetime integer is out of range", K(ret), K(int64));
} else if (int64 < power_of_10[8]) {
// YYYYMMDD.
parts[DT_MDAY] = static_cast<int32_t>(int64 % power_of_10[2]); int64 /= power_of_10[2];
parts[DT_MON] = static_cast<int32_t>(int64 % power_of_10[2]); int64 /= power_of_10[2];
parts[DT_YEAR] = static_cast<int32_t>(int64 % power_of_10[4]);
} else if (int64 / power_of_10[6] < power_of_10[8]) {
// YYYYMMDDHHMMSS.
parts[DT_SEC] = static_cast<int32_t>(int64 % power_of_10[2]); int64 /= power_of_10[2];
parts[DT_MIN] = static_cast<int32_t>(int64 % power_of_10[2]); int64 /= power_of_10[2];
parts[DT_HOUR] = static_cast<int32_t>(int64 % power_of_10[2]); int64 /= power_of_10[2];
parts[DT_MDAY] = static_cast<int32_t>(int64 % power_of_10[2]); int64 /= power_of_10[2];
parts[DT_MON] = static_cast<int32_t>(int64 % power_of_10[2]); int64 /= power_of_10[2];
parts[DT_YEAR] = static_cast<int32_t>(int64 % power_of_10[4]);
} else {
ret = OB_INVALID_DATE_VALUE;
LOG_WARN("datetime integer is out of range", K(ret), K(int64));
}
if (OB_SUCC(ret)) {
apply_date_year2_rule(parts[0]);
if (OB_FAIL(validate_datetime(ob_time, is_dayofmonth, date_sql_mode))) {
LOG_WARN("datetime is invalid or out of range", K(ret), K(int64));
} else if (ZERO_DATE != parts[DT_DATE]) {
parts[DT_DATE] = ob_time_to_date(ob_time);
}
}
return ret;
}
El código anterior representa el formato que OceanBase solo puede reconocer:
- AAAAMMDD
- AAAAMMDDHHMMSS
Una vez que no esté en el formato anterior, se generará un error. Como puede ver en la figura a continuación, solo 20230816
y 20230819111111
son elegibles.
causas del problema
Problema 3: Los resultados detectados por SELECT no son los esperados
bigint
Una "comparación" con datetime
el tipo implica una conversión implícita que conduce a resultados impredecibles.
select * from renzy where EXPIRE_AT < CURRENT_TIMESTAMP;
El valor enEXPIRE_AT
esbigint
de tipo y el valor es1716040750
.- Si el valor
1716040750
no puede coincidir con el formato especificado, se lanzaráint_to_ob_time_with_date
una alarma " el número entero de fecha y hora está fuera de rango ", que también se puede confirmar en el registro. - Aquí, se debe generar un error cuando se ejecuta SQL, y el resultado no se debe mostrar, pero para que el OB sea compatible con MySQL, se selecciona el valor "incorrecto" para que se envíe.
- ¿Por qué MySQL no informa de un error? Después de que la conversión falla en MySQL, en realidad habrá un valor predeterminado El valor predeterminado utilizado para la falla de conversión del comportamiento de OB mencionado anteriormente debe ser compatible con MySQL, por lo que se cumple la condición
0
y se muestra el resultado.WHERE
0 < 1686042749
SELECT
- ¿Por qué MySQL no informa de un error? Después de que la conversión falla en MySQL, en realidad habrá un valor predeterminado El valor predeterminado utilizado para la falla de conversión del comportamiento de OB mencionado anteriormente debe ser compatible con MySQL, por lo que se cumple la condición
Pregunta 1: ¿Por qué la instrucción UPDATE puede arrojar un error?
- Debido a que OB habilita
SQL_MODE
el modo estricto de forma predeterminada, si se produce una conversión implícita y la conversión falla (se usa el valor predeterminado), el modo estricto de OB tiene una capa de protección en comparación con MySQL y se prohibirá la ejecución de SQL. - MySQL
SQL_MODE
puede ejecutar "éxito" en modo estricto.
Pregunta 2: la clave principal en la condición WHERE de la instrucción UPDATE coincide con un valor inexistente sin informar un error
UPDATE
Se utiliza el operadortable get
, y después de que la consulta equivalente no encuentre el resultado, no es necesario aplicarfilter
las siguientes condiciones. Esto explica el fracaso esporádico de los equipos de proyecto.
La última pregunta: ¿Por qué ACTUALIZAR informa directamente un error, pero SELECCIONAR puede encontrar un "valor incorrecto"?
Este error solo se arroja al error obs.log
.
Mi conjetura aquí es que UPDATE
el modo estricto solo se seguirá cuando, pero SELECT
no es necesario seguirlo, de acuerdo con el enlace de llamada mencionado anteriormente:
- ACTUALIZAR: common_int_datetime -> int_to_datetime -> int_to_ob_time_with_date
- SELECCIONE: int_to_datetime -> int_to_ob_time_with_date
UPDATE
Hay más capas superiores common_int_datetime
y la entrada de llamada es diferente. De acuerdo con este método, se supone aproximadamente que es SQL_MODE
causado por el modo estricto de .
Verifique la conjetura, parece que SQL_MODE
después de borrar, UPDATE
se puede insertar con éxito.
MySQL [test]> select @@sql_mode;
+--------------------------------------------+
| @@sql_mode |
+--------------------------------------------+
| STRICT_ALL_TABLES,NO_ZERO_IN_DATE |
+--------------------------------------------+
1 row in set (0.01 sec)
MySQL [test]> update renzy set at=CURRENT_TIMESTAMP, expire_at=(cast(unix_timestamp(current_timestamp(3)) as unsigned) + 30000000), order_id= '0:[email protected]' where id = '1' and (expire_at < CURRENT_TIMESTAMP or order_id = '0:[email protected]');
ERROR 1292 (22007): Incorrect value
MySQL [test]> set sql_mode='';
Query OK, 0 rows affected (0.01 sec)
MySQL [test]> update renzy set at=CURRENT_TIMESTAMP, expire_at=(cast(unix_timestamp(current_timestamp(3)) as unsigned) + 30000000), order_id= '0:[email protected]' where id = '1' and (expire_at < CURRENT_TIMESTAMP or order_id = '0:[email protected]');
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
Puede haber un problema con mi conjetura, pero mi capacidad personal es limitada y todos pueden corregir y discutir juntos.
en conclusión
-
Deje que el equipo del proyecto del cliente reescriba la lógica SQL.
-
EXPIRE_AT < CURRENT_TIMESTAMP cambiado a
-
EXPIRE_AT < unix_timestamp(CURRENT_TIMESTAMP)
-
-
Haga hincapié en la especificación de SQL y evite las conversiones implícitas.
Acerca de SQLE
El SQLE de la comunidad de código abierto de Akson es una herramienta de auditoría de SQL para usuarios y administradores de bases de datos, que admite la auditoría de escenarios múltiples, admite procesos en línea estandarizados, admite de forma nativa la auditoría de MySQL y tiene tipos de bases de datos escalables.
SQLE obtener
tipo | DIRECCIÓN |
---|---|
Repositorio | https://github.com/actiontech/sqle |
documento | https://actiontech.github.io/sqle-docs/ |
lanzamiento de noticias | https://github.com/actiontech/sqle/releases |
Documentación de desarrollo del complemento de auditoría de datos | https://actiontech.github.io/sqle-docs-cn/3.modules/3.7_auditplugin/auditplugin_development.html |