Pregunta

Estaba ayudando a algunos colegas míos con un problema de SQL. Principalmente querían mover todas las filas de la tabla A a la tabla B (ambas tablas tienen las mismas columnas (nombres y tipos)). Aunque esto se hizo en Oracle 11g, no creo que realmente importe.

Su implementación ingenua inicial fue algo así como

BEGIN
  INSERT INTO B SELECT * FROM A
  DELETE FROM A
  COMMIT;
END

Su preocupación era si se hicieron INSERTOS en la tabla A durante la copia de A a B y el " ELIMINAR DE A " (o TRUNCATE para lo que valía) causaría la pérdida de datos (eliminando las filas insertadas más nuevas en A).

Por supuesto, recomendé rápidamente almacenar los ID de las filas copiadas en una tabla temporal y luego eliminar solo las filas en A que coincidían con los IDS en la tabla temporal.

Sin embargo, por curiosidad, realizamos una pequeña prueba agregando un comando de espera (no recuerdo la sintaxis PL / SQL) entre INSERT y DELETE. Luego, desde una conexión diferente, insertaríamos filas DURANTE LA ESPERA .

Observamos que fue una pérdida de datos al hacerlo. Reproduje todo el contexto en SQL Server y lo envolví todo en una transacción, pero los datos nuevos y frescos también se perdieron en SQL Server. Esto me hizo pensar que hay un error / falla sistemática en el enfoque inicial.

Sin embargo, no puedo decir si fue el hecho de que la TRANSACCIÓN no estaba (¿de alguna manera?) aislada de los nuevos INSERT nuevos o el hecho de que los INSERT vinieron durante el comando WAIT.

Al final se implementó utilizando la tabla temporal sugerida por mí, pero no pudimos obtener la respuesta a "Por qué la pérdida de datos". ¿Sabes por qué?

¿Fue útil?

Solución

Dependiendo de su nivel de aislamiento, seleccionar todas las filas de una tabla no evita nuevas inserciones, solo bloqueará las filas que lea. En SQL Server, si usa el nivel de aislamiento serializable, evitará nuevas filas si se hubieran incluido en su consulta de selección.

http://msdn.microsoft.com/en-us/library /ms173763.aspx -

SERIALIZABLE Especifica lo siguiente:

  • Las declaraciones no pueden leer datos que han sido modificados pero aún no confirmados por otras transacciones.

  • Ninguna otra transacción puede modificar los datos que ha leído la transacción actual hasta que se complete la transacción actual.

  • Otras transacciones no pueden insertar nuevas filas con valores de clave que caerían en el rango de claves leídas por cualquier declaración en la transacción actual hasta que se complete la transacción actual.

Otros consejos

No puedo hablar con la estabilidad de la transacción, pero un enfoque alternativo sería eliminar el segundo paso de la tabla de origen donde exista (seleccionar identificadores de la tabla de destino).

Perdone la sintaxis, no he probado este código, pero debería poder tener la idea:

INSERT INTO B SELECT * FROM A;

DELETE FROM A WHERE EXISTS (SELECT B.<primarykey> FROM B WHERE B.<primarykey> = A.<primarykey>);

De esta forma, está utilizando el motor relacional para garantizar que no se eliminen datos más nuevos y que no necesita realizar los dos pasos en una transacción.

Actualización: sintaxis corregida en subconsulta

Esto se puede lograr en Oracle usando:

Alter session set isolation_level=serializable;

Esto se puede configurar en PL / SQL usando EJECUTAR INMEDIATO:

BEGIN
    EXECUTE IMMEDIATE 'Alter session set isolation_level=serializable';
    ...
END;

Consulte Pregunte a Tom: sobre los niveles de aislamiento de la transacción

Es solo la forma en que funcionan las transacciones. Debe elegir el nivel de aislamiento correcto para la tarea en cuestión.

Estás haciendo INSERT y DELETE en la misma transacción. No menciona la transacción en modo de aislamiento que está utilizando, pero probablemente sea "lectura confirmada". Esto significa que el comando DELETE verá los registros que se confirmaron mientras tanto. Para este tipo de trabajo, es mucho mejor usar el tipo de transacción 'instantánea', porque entonces INSERT y DELETE sabrían sobre el mismo conjunto de registros, solo aquellos y nada más.

no sé si esto es relevante, pero en SQL Server la sintaxis es

begin tran
....
commit

no solo 'comenzar'

Debe establecer el nivel de aislamiento de su transacción para que las inserciones de otra transacción no afecten su transacción. No sé cómo hacer eso en Oracle.

En Oracle, el nivel de aislamiento de transacción predeterminado es lectura confirmada. Básicamente, eso significa que Oracle devuelve los resultados tal como existían en el SCN (número de cambio del sistema) cuando comenzó su consulta. Establecer el nivel de aislamiento de la transacción en serializable significa que el SCN se captura al comienzo de la transacción, por lo que todas las consultas en su transacción devuelven datos a partir de ese SCN. Eso garantiza resultados consistentes independientemente de lo que estén haciendo otras sesiones y transacciones. Por otro lado, puede haber un costo en el hecho de que Oracle puede determinar que no puede serializar su transacción debido a la actividad que realizan otras transacciones, por lo que tendría que manejar ese tipo de error.

El enlace de Tony a la discusión de AskTom incluye muchos más detalles sobre todo esto. Lo recomiendo encarecidamente.

Sí Milán, no he especificado el nivel de aislamiento de la transacción. Supongo que es el nivel de aislamiento predeterminado que no sé cuál es. Ni en Oracle 11g ni en SQL Server 2005.

Además, el INSERT que se realizó durante el comando WAIT (en la segunda conexión) fue NOT dentro de una transacción. ¿Debería haber sido para evitar esta pérdida de datos?

Este es el comportamiento estándar del modo predeterminado de lectura confirmada, como se mencionó anteriormente. El comando WAIT solo causa un retraso en el procesamiento, no hay ningún enlace a ningún manejo de transacciones DB.

Para solucionar el problema, puede:

  1. establezca el nivel de aislamiento en serializable, pero luego puede obtener errores de ORA, que debe manejar con reintentos. Además, puede obtener un grave impacto en el rendimiento.
  2. use una tabla temporal para almacenar los valores primero
  3. si los datos no son demasiado grandes para caber en la memoria, puede usar una cláusula RETURNING para COLECCIÓN A GRANEL EN UNA tabla anidada y eliminar solo si la fila está presente en la tabla anidada.

Alternativamente, puede usar el aislamiento de instantáneas para detectar actualizaciones perdidas:

Cuando el aislamiento de instantáneas ayuda y cuándo duele

    I have written a sample code:-

    First run this on Oracle DB:-


     Create table AccountBalance
        (
              id integer Primary Key,
              acctName varchar2(255) not null,
              acctBalance integer not null,
              bankName varchar2(255) not null
        );

        insert into AccountBalance values (1,'Test',50000,'Bank-a');

    Now run the below code 





 package com.java.transaction.dirtyread;
        import java.sql.Connection;
        import java.sql.DriverManager;
        import java.sql.SQLException;

        public class DirtyReadExample {

         /**
          * @param args
         * @throws ClassNotFoundException 
          * @throws SQLException 
          * @throws InterruptedException 
          */
         public static void main(String[] args) throws ClassNotFoundException, SQLException, InterruptedException {

             Class.forName("oracle.jdbc.driver.OracleDriver");
             Connection connectionPayment = DriverManager.getConnection(
                        "jdbc:oracle:thin:@localhost:1521:xe", "hr",
                        "hr");
             Connection connectionReader = DriverManager.getConnection(
                        "jdbc:oracle:thin:@localhost:1521:xe", "hr",
                        "hr");

          try {
              connectionPayment.setAutoCommit(false);
              connectionPayment.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);


          } catch (SQLException e) {
           e.printStackTrace();
          }


          Thread pymtThread=new Thread(new PaymentRunImpl(connectionPayment));
          Thread readerThread=new Thread(new ReaderRunImpl(connectionReader));

          pymtThread.start();
          Thread.sleep(2000);
          readerThread.start();

         }

        }



        package com.java.transaction.dirtyread;

        import java.sql.Connection;
        import java.sql.PreparedStatement;
        import java.sql.ResultSet;
        import java.sql.SQLException;

        public class ReaderRunImpl  implements Runnable{

         private Connection conn;

         private static final String QUERY="Select acctBalance from AccountBalance where id=1";

         public ReaderRunImpl(Connection conn){
          this.conn=conn;
         }

         @Override
         public void run() {
          PreparedStatement stmt =null; 
          ResultSet rs =null;

          try {
           stmt = conn.prepareStatement(QUERY);
           System.out.println("In Reader thread --->Statement Prepared");
           rs = stmt.executeQuery();
           System.out.println("In Reader thread --->executing");
           while (rs.next()){

            System.out.println("Balance is:" + rs.getDouble(1));

           }
           System.out.println("In Reader thread --->Statement Prepared");
           Thread.sleep(5000);
           stmt.close();
           rs.close();
           stmt = conn.prepareStatement(QUERY);
           rs = stmt.executeQuery();
           System.out.println("In Reader thread --->executing");
           while (rs.next()){

            System.out.println("Balance is:" + rs.getDouble(1));

           }
           stmt.close();
           rs.close();
           stmt = conn.prepareStatement(QUERY);
           rs = stmt.executeQuery();
           System.out.println("In Reader thread --->executing");
           while (rs.next()){

            System.out.println("Balance is:" + rs.getDouble(1));

           }
          } catch (SQLException | InterruptedException e) {
           e.printStackTrace();
          }finally{
           try {
            stmt.close();
            rs.close();
           } catch (SQLException e) {
            e.printStackTrace();
           }   
          }
         }

        }

        package com.java.transaction.dirtyread;
        import java.sql.Connection;
        import java.sql.PreparedStatement;
        import java.sql.SQLException;

        public class PaymentRunImpl implements Runnable{

         private Connection conn;

         private static final String QUERY1="Update AccountBalance set acctBalance=40000 where id=1";
         private static final String QUERY2="Update AccountBalance set acctBalance=30000 where id=1";
         private static final String QUERY3="Update AccountBalance set acctBalance=20000 where id=1";
         private static final String QUERY4="Update AccountBalance set acctBalance=10000 where id=1";

         public PaymentRunImpl(Connection conn){
          this.conn=conn;
         }

         @Override
         public void run() {
          PreparedStatement stmt = null;

          try {   
           stmt = conn.prepareStatement(QUERY1);
           stmt.execute();
           System.out.println("In Payment thread --> executed");
           Thread.sleep(3000);
           stmt = conn.prepareStatement(QUERY2);
           stmt.execute();
           System.out.println("In Payment thread --> executed");
           Thread.sleep(3000);
           stmt = conn.prepareStatement(QUERY3);
           stmt.execute();
           System.out.println("In Payment thread --> executed");
           stmt = conn.prepareStatement(QUERY4);
           stmt.execute();
           System.out.println("In Payment thread --> executed");

           Thread.sleep(5000);
            //case 1
           conn.rollback();
           System.out.println("In Payment thread --> rollback");
          //case 2
           //conn.commit();
          // System.out.println("In Payment thread --> commit");
          } catch (SQLException e) {
           e.printStackTrace();
          } catch (InterruptedException e) {    
           e.printStackTrace();
          }finally{
           try {
            stmt.close();
           } catch (SQLException e) {
            e.printStackTrace();
           }
          }
         }

        }

    Output:-
    In Payment thread --> executed
    In Reader thread --->Statement Prepared
    In Reader thread --->executing
    Balance is:50000.0
    In Reader thread --->Statement Prepared
    In Payment thread --> executed
    In Payment thread --> executed
    In Payment thread --> executed
    In Reader thread --->executing
    Balance is:50000.0
    In Reader thread --->executing
    Balance is:50000.0
    In Payment thread --> rollback

U puede probarlo insertando nuevas filas según lo definido por Oracle: -  Una lectura fantasma ocurre cuando la transacción A recupera un conjunto de filas que satisfacen una condición dada, la transacción B posteriormente inserta o actualiza una fila de modo que la fila ahora cumpla la condición en la transacción A, y la transacción A luego repite la recuperación condicional. La transacción A ahora ve una fila adicional. Esta fila se conoce como fantasma. Evitará el escenario anterior, así como he usado TRANSACTION_SERIALIZABLE. Establecerá el bloqueo más estricto en el Oracle. Oracle solo admite 2 tipos de niveles de aislamiento de transacciones: - TRANSACTION_READ_COMMITTED y TRANSACTION_SERIALIZABLE.

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