Pregunta

¿Hay una diferencia de rendimiento medible entre usar INT vs. VARCHAR como clave principal en MySQL? Me gustaría usar VARCHAR como clave principal para las listas de referencias (piense en los Estados Unidos, los códigos de país) y un compañero de trabajo no cederá en INT AUTO_INCREMENT como clave principal para todas las tablas.

Mi argumento, como se detalla aquí , es que la diferencia de rendimiento entre INT y VARCHAR es insignificante, ya que cada referencia de clave externa INT requerirá un JOIN para que la referencia tenga sentido, una clave VARCHAR presentará directamente la información.

Entonces, ¿alguien tiene experiencia con este caso de uso en particular y los problemas de rendimiento asociados con él?

¿Fue útil?

Solución

Usted hace un buen punto de que puede evitar cierto número de consultas unidas utilizando lo que se llama clave natural en lugar de una clave sustituta . Solo usted puede evaluar si el beneficio de esto es significativo en su aplicación.

Es decir, puede medir las consultas en su aplicación que son las más importantes para ser rápido, porque funcionan con grandes volúmenes de datos o se ejecutan con mucha frecuencia. Si estas consultas se benefician de la eliminación de una unión, y no sufren al usar una clave primaria varchar, hágalo.

No utilice ninguna de las estrategias para todas las tablas en su base de datos. Es probable que en algunos casos, una clave natural sea mejor, pero en otros casos una clave sustituta es mejor.

Otras personas señalan que es raro en la práctica que una clave natural nunca cambie o tenga duplicados, por lo que las claves sustitutas generalmente valen la pena.

Otros consejos

No se trata de rendimiento. Se trata de lo que hace una buena clave primaria. Único e inmutable en el tiempo. Puede pensar que una entidad como un código de país nunca cambia con el tiempo y sería un buen candidato para una clave principal. Pero la experiencia amarga es que rara vez es así.

INT AUTO_INCREMENT cumple con el "único e inmutable en el tiempo" condición. De ahí la preferencia.

Depende de la longitud. Si el varchar tendrá 20 caracteres y el int es 4, entonces si usa un int, su índice tendrá CINCO veces más nodos por página de espacio de índice en el disco ... Eso significa que atravesar el índice requerirá una quinta parte de lecturas físicas y / o lógicas.

Entonces, si el rendimiento es un problema, dada la oportunidad, use siempre una clave integral no significativa (llamada sustituto) para sus tablas y para claves externas que hacen referencia a las filas en estas tablas ...

Al mismo tiempo , para garantizar la coherencia de los datos, cada tabla donde sea importante debe también tener una clave alternativa no numérica significativa, (o índice único) para garantizar que no se puedan insertar filas duplicadas (duplicado basado en atributos de tabla significativos).

Para el uso específico del que está hablando (como búsquedas de estado) realmente no importa porque el tamaño de la tabla es muy pequeño. En general, no hay impacto en el rendimiento de los índices en tablas con menos de unos pocos mil filas ...

Absolutamente no.

He realizado varias ... varias ... comprobaciones de rendimiento entre INT, VARCHAR y CHAR.

La tabla de 10 millones de registros con una CLAVE PRIMARIA (única y agrupada) tenía exactamente la misma velocidad y rendimiento (y costo de subárbol) sin importar cuál de los tres usé.

Dicho esto ... usa lo que sea mejor para tu aplicación. No te preocupes por el rendimiento.

Estaba un poco molesto por la falta de puntos de referencia para esto en línea, así que realicé una prueba yo mismo.

Tenga en cuenta que no lo hago de forma regular, así que compruebe mi configuración y mis pasos para ver si hay algún factor que pueda haber influido en los resultados involuntariamente, y publique sus inquietudes en los comentarios.

La configuración fue la siguiente:

  • CPU Intel® Core ™ i7-7500U @ 2.70GHz × 4
  • 15.6 GiB RAM, de los cuales me aseguré de que alrededor de 8 GB estuvieran libres durante la prueba.
  • Unidad SSD de 148,6 GB, con mucho espacio libre.
  • Ubuntu 16.04 de 64 bits
  • MySQL Ver 14.14 Distrib 5.7.20, para Linux (x86_64)

Las tablas:

create table jan_int (data1 varchar(255), data2 int(10), myindex tinyint(4)) ENGINE=InnoDB;
create table jan_int_index (data1 varchar(255), data2 int(10), myindex tinyint(4), INDEX (myindex)) ENGINE=InnoDB;
create table jan_char (data1 varchar(255), data2 int(10), myindex char(6)) ENGINE=InnoDB;
create table jan_char_index (data1 varchar(255), data2 int(10), myindex char(6), INDEX (myindex)) ENGINE=InnoDB;
create table jan_varchar (data1 varchar(255), data2 int(10), myindex varchar(63)) ENGINE=InnoDB;
create table jan_varchar_index (data1 varchar(255), data2 int(10), myindex varchar(63), INDEX (myindex)) ENGINE=InnoDB;

Luego, llené 10 millones de filas en cada tabla con un script PHP cuya esencia es así:

$pdo = get_pdo();

$keys = [ 'alabam', 'massac', 'newyor', 'newham', 'delawa', 'califo', 'nevada', 'texas_', 'florid', 'ohio__' ];

for ($k = 0; $k < 10; $k++) {
    for ($j = 0; $j < 1000; $j++) {
        $val = '';
        for ($i = 0; $i < 1000; $i++) {
            $val .= '("' . generate_random_string() . '", ' . rand (0, 10000) . ', "' . ($keys[rand(0, 9)]) . '"),';
        }
        $val = rtrim($val, ',');
        $pdo->query('INSERT INTO jan_char VALUES ' . $val);
    }
    echo "\n" . ($k + 1) . ' millon(s) rows inserted.';
}

Para las tablas int , el bit ($ keys [rand (0, 9)]) se reemplazó por solo rand (0, 9) , y para las tablas varchar , utilicé nombres completos de estados de EE. UU., sin cortarlos ni extenderlos a 6 caracteres. generate_random_string () genera una cadena aleatoria de 10 caracteres.

Luego ejecuté en MySQL:

  • SET SESSION query_cache_type = 0;
  • Para la tabla jan_int :
    • SELECT count (*) FROM jan_int WHERE myindex = 5;
    • SELECT BENCHMARK (1000000000, (SELECT count (*) FROM jan_int WHERE myindex = 5));
  • Para otras tablas, igual que la anterior, con myindex = 'califo' para tablas char y myindex = 'california' para tablas varchar .

Tiempos de la consulta BENCHMARK en cada tabla:

  • jan_int: 21.30 segundos
  • jan_int_index: 18.79 segundos
  • jan_char: 21,70 segundos
  • jan_char_index: 18.85 segundos
  • jan_varchar: 21,76 segundos
  • jan_varchar_index: 18.86 segundos

Con respecto a la tabla & amp; tamaños de índice, aquí está el resultado de mostrar el estado de la tabla de janperformancetest; (con algunas columnas no mostradas):

|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Name              | Engine | Version | Row_format | Rows    | Avg_row_length | Data_length | Max_data_length | Index_length | Data_free | Auto_increment | Collation              |
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| jan_int           | InnoDB |      10 | Dynamic    | 9739094 |             43 |   422510592 |               0 |            0 |   4194304 |           NULL | utf8mb4_unicode_520_ci |  
| jan_int_index     | InnoDB |      10 | Dynamic    | 9740329 |             43 |   420413440 |               0 |    132857856 |   7340032 |           NULL | utf8mb4_unicode_520_ci |   
| jan_char          | InnoDB |      10 | Dynamic    | 9726613 |             51 |   500170752 |               0 |            0 |   5242880 |           NULL | utf8mb4_unicode_520_ci |  
| jan_char_index    | InnoDB |      10 | Dynamic    | 9719059 |             52 |   513802240 |               0 |    202342400 |   5242880 |           NULL | utf8mb4_unicode_520_ci |  
| jan_varchar       | InnoDB |      10 | Dynamic    | 9722049 |             53 |   521142272 |               0 |            0 |   7340032 |           NULL | utf8mb4_unicode_520_ci |   
| jan_varchar_index | InnoDB |      10 | Dynamic    | 9738381 |             49 |   486539264 |               0 |    202375168 |   7340032 |           NULL | utf8mb4_unicode_520_ci | 
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

Mi conclusión es que no hay diferencia de rendimiento para este caso de uso en particular.

Para códigos cortos, probablemente no haya diferencia. Esto es especialmente cierto ya que es probable que la tabla que contiene estos códigos sea muy pequeña (un par de miles de filas como máximo) y no cambie con frecuencia (cuándo es la última vez que agregamos un nuevo Estado de EE. UU.).

Para tablas más grandes con una variación más amplia entre la clave, esto puede ser peligroso. Piense en utilizar la dirección de correo electrónico / nombre de usuario de una tabla de usuario, por ejemplo. Qué sucede cuando tiene unos pocos millones de usuarios y algunos de esos usuarios tienen nombres largos o direcciones de correo electrónico. Ahora, cada vez que necesite unirse a esta tabla con esa clave, se vuelve mucho más costoso.

En cuanto a la clave primaria, cualquier cosa que físicamente haga que una fila sea única debe determinarse como la clave primaria.

Para una referencia como clave externa, usar un entero de incremento automático como sustituto es una buena idea por dos razones principales.
 - Primero, generalmente hay menos gastos generales incurridos en la unión.
 - En segundo lugar, si necesita actualizar la tabla que contiene el varchar único, entonces la actualización debe descender en cascada a todas las tablas secundarias y actualizarlas todas, así como los índices, mientras que con el sustituto int, solo tiene que actualizar el tabla maestra y sus índices.

El inconveniente de usar el sustituto es que posiblemente podría permitir cambiar el significado del sustituto:

ex.
id value
1 A
2 B
3 C

Update 3 to D
id value
1 A
2 B
3 D

Update 2 to C
id value
1 A
2 C
3 D

Update 3 to B
id value
1 A
2 C
3 B

Todo depende de lo que realmente necesita preocuparse en su estructura y lo que significa más.

Casos comunes donde un sustituto AUTO_INCREMENT duele:

Un patrón de esquema común es una asignación de muchos a muchos :

CREATE TABLE map (
    id ... AUTO_INCREMENT,
    foo_id ...,
    bar_id ...,
    PRIMARY KEY(id),
    UNIQUE(foo_id, bar_id),
    INDEX(bar_id) );

El rendimiento de este patrón es mucho mejor, especialmente cuando se usa InnoDB:

CREATE TABLE map (
    # No surrogate
    foo_id ...,
    bar_id ...,
    PRIMARY KEY(foo_id, bar_id),
    INDEX      (bar_id, foo_id) );

¿Por qué?

  • Las claves secundarias de InnoDB necesitan una búsqueda adicional; moviendo el par al PK, eso se evita en una dirección.
  • El índice secundario está "cubriendo", por lo que no necesita la búsqueda adicional.
  • Esta tabla es más pequeña debido a la eliminación de id y un índice.

Otro caso ( país ):

country_id INT ...
-- versus
country_code CHAR(2) CHARACTER SET ascii

Con demasiada frecuencia, el novato normaliza country_code en un INT de 4 bytes en lugar de utilizar una cadena 'natural' de 2 bytes, casi inmutable, de 2 bytes. Más rápido, más pequeño, menos uniones, más legible.

En HauteLook, cambiamos muchas de nuestras tablas para usar claves naturales. Experimentamos un aumento en el rendimiento en el mundo real. Como mencionas, muchas de nuestras consultas ahora usan menos combinaciones, lo que hace que las consultas sean más efectivas. Incluso usaremos una clave primaria compuesta si tiene sentido. Dicho esto, algunas tablas son más fáciles de trabajar si tienen una clave sustituta.

Además, si permite que las personas escriban interfaces en su base de datos, una clave sustituta puede ser útil. El tercero puede confiar en el hecho de que la clave sustituta cambiará solo en circunstancias muy raras.

La pregunta es sobre MySQL, así que digo que hay una diferencia significativa. Si se tratara de Oracle (que almacena números como una cadena, sí, no podía creerlo al principio), entonces no habría mucha diferencia.

El almacenamiento en la tabla no es el problema, pero sí lo es actualizar y hacer referencia al índice. Las consultas que implican buscar un registro en función de su clave principal son frecuentes: desea que ocurran lo más rápido posible porque suceden con tanta frecuencia.

La cosa es que una CPU trata con 4 bytes y enteros de 8 bytes de forma natural, en silicio . Es REALMENTE rápido comparar dos enteros: ocurre en uno o dos ciclos de reloj.

Ahora observe una cadena: está compuesta por muchos caracteres (más de un byte por carácter en estos días). La comparación de dos cadenas de precedencia no se puede hacer en uno o dos ciclos. En cambio, los caracteres de las cadenas deben iterarse hasta que se encuentre una diferencia. Estoy seguro de que hay trucos para hacerlo más rápido en algunas bases de datos, pero eso es irrelevante aquí porque la CPU realiza una comparación int de forma natural y ultrarrápida en silicio.

Mi regla general: cada clave principal debe ser un INT de incremento automático, especialmente en aplicaciones OO que usan un ORM (Hibernate, Datanucleus, lo que sea) donde hay muchas relaciones entre objetos; por lo general, siempre se implementarán como un FK simple y el la capacidad de la base de datos para resolverlos rápidamente es importante para su aplicación s receptividad.

Me enfrenté al mismo dilema. Hice un DW (esquema de constelación) con 3 tablas de hechos, accidentes de tráfico, vehículos en accidentes y víctimas en accidentes. Los datos incluyen todos los accidentes registrados en el Reino Unido desde 1979 hasta 2012, y 60 tablas de dimensiones. En total, unos 20 millones de registros.

Relaciones de tablas de hechos:

+----------+          +---------+
| Accident |>--------<| Vehicle |
+-----v----+ 1      * +----v----+
     1|                    |1
      |    +----------+    |
      +---<| Casualty |>---+
         * +----------+ *

RDMS: MySQL 5.6

Nativamente, el índice de Accidente es un varchar (números y letras), con 15 dígitos. Traté de no tener claves sustitutas, una vez que los índices de accidentes nunca cambiarían. En una computadora i7 (8 núcleos), el DW se volvió demasiado lento para consultar después de 12 millones de registros de carga, dependiendo de las dimensiones. Después de mucho volver a trabajar y agregar claves sustitutas bigint, obtuve un aumento promedio del rendimiento de velocidad del 20%. Sin embargo, a la baja ganancia de rendimiento, pero prueba válida. Estoy trabajando en MySQL tuning y clustering.

No estoy seguro de las implicaciones de rendimiento, pero parece que un posible compromiso, al menos durante el desarrollo, sería incluir tanto el sustituto entero "subrogado" auto-incrementado. clave, así como su intención, única, natural, natural llave. Esto le daría la oportunidad de evaluar el rendimiento, así como otros posibles problemas, incluida la posibilidad de cambiar las claves naturales.

Como de costumbre, no hay respuestas generales. '¡Depende!' y no estoy siendo gracioso Comprendí que la pregunta original era para las claves en tablas pequeñas, como Country (identificación de enteros o código char / varchar) que es una clave externa para una tabla potencialmente enorme como la tabla de dirección / contacto.

Hay dos escenarios aquí cuando desea recuperar los datos de la base de datos. Primero es un tipo de consulta de lista / búsqueda donde desea enumerar todos los contactos con códigos o nombres de estado y país (los identificadores no ayudarán y, por lo tanto, necesitarán una búsqueda). El otro es un escenario de obtención en la clave principal que muestra un solo registro de contacto donde se debe mostrar el nombre del estado, país.

Para este último, probablemente no importa en qué se basa el FK ya que estamos reuniendo tablas para un solo registro o unos pocos registros y en lecturas clave. El primer escenario (búsqueda o lista) puede verse afectado por nuestra elección. Dado que es necesario mostrar el país (al menos un código reconocible y tal vez incluso la búsqueda en sí misma incluye un código de país), no tener que unirse a otra tabla a través de una clave sustituta potencialmente (solo estoy siendo cauteloso aquí porque en realidad no he probado esto, pero parece altamente probable) mejorar el rendimiento; a pesar del hecho de que ciertamente ayuda con la búsqueda.

Como los códigos son de tamaño pequeño, no más de 3 caracteres generalmente para el país y el estado, puede estar bien usar las claves naturales como claves foráneas en este escenario.

El otro escenario donde las claves dependen de valores de varchar más largos y quizás de tablas más grandes; la clave sustituta probablemente tiene la ventaja.

Permítanme decir que sí, definitivamente hay una diferencia, teniendo en cuenta el alcance del rendimiento (definición original):

1- El uso de sustituto int es más rápido en la aplicación porque no necesita usar ToUpper (), ToLower (), ToUpperInvarient () o ToLowerInvarient () en su código o en su consulta y estas 4 funciones tienen diferentes puntos de referencia de rendimiento . Consulte las reglas de rendimiento de Microsoft sobre esto. (rendimiento de la aplicación)

2- El uso de sustituto int garantiza no cambiar la clave con el tiempo. Incluso los códigos de país pueden cambiar, vea Wikipedia cómo los códigos ISO cambiaron con el tiempo. Eso llevaría mucho tiempo cambiar la clave primaria para los subárboles. (rendimiento del mantenimiento de datos)

3- Parece que hay problemas con las soluciones ORM, como NHibernate cuando PK / FK no es int. (rendimiento del desarrollador)

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top